feat(api, web): implement port ACL and host firewall snapshot features
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 1m43s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

- Added support for managing desired L4 port ACL rules for Linux agents, allowing for open/close actions on specified ports.
- Introduced a new endpoint for CRUD operations on port rules, enhancing the API's capabilities for agent management.
- Implemented functionality to collect and report host firewall snapshots, capturing observed rules and listeners for better monitoring.
- Updated the agent detail view to include tabs for managing port ACLs and viewing host firewall data, improving user experience.
- Enhanced documentation to reflect the new features and API changes, ensuring clarity for users and developers.

These changes significantly improve the management and visibility of firewall rules and port access control for agents.
This commit is contained in:
Denozordec
2026-08-11 15:08:00 +07:00
parent c5069fbdaf
commit 43f5ac2525
22 changed files with 2853 additions and 17 deletions
+168 -1
View File
@@ -233,6 +233,42 @@ export const applyReportPortHitSchema = z.object({
packets: z.number().int().nonnegative(),
})
export const hostFwOwnershipSchema = z.enum(['evofw', 'foreign'])
export const hostFwBackendSchema = z.enum([
'nft',
'iptables',
'ufw',
'firewalld',
'listener',
])
export const hostFwRuleSchema = z.object({
ownership: hostFwOwnershipSchema,
backend: hostFwBackendSchema,
table: z.string().max(128).optional(),
chain: z.string().max(128).optional(),
action: z.string().max(64).optional(),
protocol: z.string().max(16).optional(),
dport: z.string().max(64).optional(),
sport: z.string().max(64).optional(),
saddr: z.string().max(128).optional(),
daddr: z.string().max(128).optional(),
comment: z.string().max(256).optional(),
raw: z.string().max(512),
})
export const hostListenerSchema = z.object({
protocol: z.string().max(16),
port: z.number().int().min(0).max(65535),
address: z.string().max(128),
process: z.string().max(128).optional(),
})
export const hostFirewallPayloadSchema = z.object({
rules: z.array(hostFwRuleSchema).max(500).default([]),
listeners: z.array(hostListenerSchema).max(200).default([]),
})
export const applyReportBodySchema = z.object({
status: z.string(),
prefix_count: z.number().int().optional(),
@@ -245,6 +281,129 @@ export const applyReportBodySchema = z.object({
ip_hits: z.array(applyReportIpHitSchema).max(200).optional(),
/** Linux nft dynamic set per-(ip, proto, dport) deny hits (top-N). */
port_hits: z.array(applyReportPortHitSchema).max(500).optional(),
/** Observed host firewall + listeners (Linux). */
host_firewall: hostFirewallPayloadSchema.optional(),
})
export const agentPortRuleActionSchema = z.enum(['open', 'close'])
export const agentPortRuleProtocolSchema = z.enum(['tcp', 'udp', 'both'])
export const agentPortRuleSrcKindSchema = z.enum(['all', 'cidr', 'list'])
export const agentPortRuleSchema = z.object({
id: z.string(),
agent_id: z.string(),
action: agentPortRuleActionSchema,
protocol: agentPortRuleProtocolSchema,
port_start: z.number().int().min(1).max(65535),
port_end: z.number().int().min(1).max(65535),
src_kind: agentPortRuleSrcKindSchema,
src_cidr: z.string().nullable().optional(),
list_id: z.string().nullable().optional(),
list_name: z.string().nullable().optional(),
enabled: z.boolean(),
comment: z.string().nullable().optional(),
priority: z.number().int(),
created_at: z.string(),
updated_at: z.string(),
})
export const createAgentPortRuleBodySchema = z
.object({
action: agentPortRuleActionSchema,
protocol: agentPortRuleProtocolSchema.default('tcp'),
port_start: z.number().int().min(1).max(65535),
port_end: z.number().int().min(1).max(65535).optional(),
src_kind: agentPortRuleSrcKindSchema.default('all'),
src_cidr: z.string().min(1).max(64).optional(),
list_id: z.string().min(1).optional(),
enabled: z.boolean().optional().default(true),
comment: z.string().max(500).optional(),
priority: z.number().int().optional().default(100),
})
.superRefine((v, ctx) => {
const end = v.port_end ?? v.port_start
if (end < v.port_start) {
ctx.addIssue({
code: 'custom',
message: 'port_end must be >= port_start',
path: ['port_end'],
})
}
if (v.src_kind === 'cidr' && !v.src_cidr?.trim()) {
ctx.addIssue({
code: 'custom',
message: 'src_cidr required when src_kind=cidr',
path: ['src_cidr'],
})
}
if (v.src_kind === 'list' && !v.list_id?.trim()) {
ctx.addIssue({
code: 'custom',
message: 'list_id required when src_kind=list',
path: ['list_id'],
})
}
})
export const updateAgentPortRuleBodySchema = z
.object({
action: agentPortRuleActionSchema.optional(),
protocol: agentPortRuleProtocolSchema.optional(),
port_start: z.number().int().min(1).max(65535).optional(),
port_end: z.number().int().min(1).max(65535).optional(),
src_kind: agentPortRuleSrcKindSchema.optional(),
src_cidr: z.string().min(1).max(64).nullable().optional(),
list_id: z.string().min(1).nullable().optional(),
enabled: z.boolean().optional(),
comment: z.string().max(500).nullable().optional(),
priority: z.number().int().optional(),
})
.refine((o) => Object.keys(o).length > 0, { message: 'empty update' })
export const importAgentPortRulesBodySchema = z
.object({
from: z.enum(['list', 'set']),
list_id: z.string().min(1).optional(),
set_id: z.string().min(1).optional(),
action: agentPortRuleActionSchema,
protocol: agentPortRuleProtocolSchema.default('tcp'),
ports: z
.array(
z.object({
port_start: z.number().int().min(1).max(65535),
port_end: z.number().int().min(1).max(65535).optional(),
}),
)
.min(1)
.max(50),
enabled: z.boolean().optional().default(true),
comment: z.string().max(500).optional(),
})
.superRefine((v, ctx) => {
if (v.from === 'list' && !v.list_id?.trim()) {
ctx.addIssue({
code: 'custom',
message: 'list_id required when from=list',
path: ['list_id'],
})
}
if (v.from === 'set' && !v.set_id?.trim()) {
ctx.addIssue({
code: 'custom',
message: 'set_id required when from=set',
path: ['set_id'],
})
}
})
/** Expanded port rule for agent policy apply_version >= 3. */
export const agentPolicyPortRuleSchema = z.object({
id: z.string(),
action: agentPortRuleActionSchema,
protocol: z.enum(['tcp', 'udp']),
port_start: z.number().int(),
port_end: z.number().int(),
src_cidrs: z.array(z.string()),
})
export const agentIpPortStatSchema = z.object({
@@ -277,6 +436,7 @@ export const agentPolicySchema = z.object({
policy_mode: policyModeSchema.optional(),
deny_cidrs: z.array(z.string()),
allow_cidrs: z.array(z.string()),
port_rules: z.array(agentPolicyPortRuleSchema).optional(),
sync_interval_sec: z.number().int(),
})
@@ -285,7 +445,7 @@ export const agentPolicyPreviewSchema = z.object({
hash: z.string(),
generation: z.number().int(),
sync_interval_sec: z.number().int(),
apply_version: z.literal(2),
apply_version: z.literal(3),
summary: z.object({
sets: z.number().int(),
rules_deny: z.number().int(),
@@ -294,6 +454,7 @@ export const agentPolicyPreviewSchema = z.object({
cidrs_allow: z.number().int(),
overrides: z.number().int(),
conflicts_dropped: z.number().int(),
port_rules: z.number().int().optional(),
}),
chain: z.array(
z.object({
@@ -310,6 +471,7 @@ export const agentPolicyPreviewSchema = z.object({
allow_cidrs: z.array(z.string()),
deny_cidrs_total: z.number().int(),
allow_cidrs_total: z.number().int(),
port_rules: z.array(agentPolicyPortRuleSchema).optional(),
})
export const dashboardStatsSchema = z.object({
@@ -387,6 +549,11 @@ export type PolicySet = z.infer<typeof policySetSchema>
export type IpOverride = z.infer<typeof ipOverrideSchema>
export type AgentPolicy = z.infer<typeof agentPolicySchema>
export type AgentPolicyPreview = z.infer<typeof agentPolicyPreviewSchema>
export type AgentPortRule = z.infer<typeof agentPortRuleSchema>
export type CreateAgentPortRuleBody = z.infer<typeof createAgentPortRuleBodySchema>
export type UpdateAgentPortRuleBody = z.infer<typeof updateAgentPortRuleBodySchema>
export type ImportAgentPortRulesBody = z.infer<typeof importAgentPortRulesBodySchema>
export type HostFirewallPayload = z.infer<typeof hostFirewallPayloadSchema>
export type DashboardStats = z.infer<typeof dashboardStatsSchema>
export type InstallLink = z.infer<typeof installLinkSchema>
export type EvobgpCommunity = z.infer<typeof evobgpCommunitySchema>