= [
+ {
+ title: t('fw.svc.name'), dataIndex: 'name', key: 'name',
+ render: (s: string, row) => {s}{row.builtin && builtin},
+ },
+ { title: t('fw.svc.proto'), dataIndex: 'proto', key: 'proto', render: (p: string) => {p} },
+ {
+ title: t('fw.svc.ports'), key: 'ports',
+ render: (_, row) => row.proto === 'tcp' || row.proto === 'udp'
+ ? {row.port_start === row.port_end ? row.port_start : `${row.port_start}-${row.port_end}`}
+ : '—',
+ },
+ { title: t('fw.svc.description'), dataIndex: 'description', key: 'desc', render: (v?: string) => v ?? '—' },
+ {
+ title: t('common.edit'), key: 'actions',
+ render: (_, row) => (
+
+
+ del.mutate(row.id)}
+ disabled={row.builtin}
+ >
+
+
+
+ ),
+ },
+ ]
+
+ return (
+ <>
+
+
+ { setEditing(null); setCreating(false) }}
+ onOk={() => { void form.submit() }}
+ confirmLoading={create.isPending || update.isPending}
+ >
+
+
+
+
+
+ p.proto !== c.proto}>
+ {({ getFieldValue }) => {
+ const proto = getFieldValue('proto') as FwService['proto']
+ if (proto !== 'tcp' && proto !== 'udp') return null
+ return (
+
+
+
+
+
+
+
+
+ )
+ }}
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/management-ui/src/pages/Firewall/index.tsx b/management-ui/src/pages/Firewall/index.tsx
new file mode 100644
index 0000000..d12ee4c
--- /dev/null
+++ b/management-ui/src/pages/Firewall/index.tsx
@@ -0,0 +1,30 @@
+import { Tabs, Typography } from 'antd'
+import { useTranslation } from 'react-i18next'
+
+import AddressObjectsTab from './AddressObjects'
+import AddressGroupsTab from './AddressGroups'
+import ServicesTab from './Services'
+import ServiceGroupsTab from './ServiceGroups'
+import RulesTab from './Rules'
+import NATRulesTab from './NATRules'
+
+export default function FirewallPage() {
+ const { t } = useTranslation()
+
+ const tabs = [
+ { key: 'rules', label: t('fw.tabs.rules'), children: },
+ { key: 'nat', label: t('fw.tabs.nat'), children: },
+ { key: 'addrObj', label: t('fw.tabs.addrObj'), children: },
+ { key: 'addrGrp', label: t('fw.tabs.addrGrp'), children: },
+ { key: 'services', label: t('fw.tabs.services'), children: },
+ { key: 'svcGrp', label: t('fw.tabs.svcGrp'), children: },
+ ]
+
+ return (
+
+ {t('fw.title')}
+ {t('fw.intro')}
+
+
+ )
+}
diff --git a/management-ui/src/pages/Firewall/types.ts b/management-ui/src/pages/Firewall/types.ts
new file mode 100644
index 0000000..9ac459c
--- /dev/null
+++ b/management-ui/src/pages/Firewall/types.ts
@@ -0,0 +1,80 @@
+// Shared types for the Firewall page tabs.
+
+export interface AddressObject {
+ id: number
+ name: string
+ kind: 'host' | 'network' | 'range' | 'fqdn'
+ value: string
+ description?: string | null
+ created_at: string
+ updated_at: string
+}
+
+export interface AddressGroup {
+ id: number
+ name: string
+ description?: string | null
+ member_ids?: number[]
+ created_at: string
+ updated_at: string
+}
+
+export interface FwService {
+ id: number
+ name: string
+ proto: 'tcp' | 'udp' | 'icmp' | 'icmpv6' | 'any'
+ port_start?: number | null
+ port_end?: number | null
+ builtin: boolean
+ description?: string | null
+}
+
+export interface ServiceGroup {
+ id: number
+ name: string
+ description?: string | null
+ member_ids?: number[]
+}
+
+export type Zone = 'wan' | 'lan' | 'dmz' | 'mgmt' | 'cluster' | 'any'
+
+export interface FwRule {
+ id: number
+ name?: string | null
+ priority: number
+ enabled: boolean
+ action: 'accept' | 'drop' | 'reject'
+ src_zone: Zone
+ src_address_object_id?: number | null
+ src_address_group_id?: number | null
+ src_cidr?: string | null
+ dst_zone: Zone
+ dst_address_object_id?: number | null
+ dst_address_group_id?: number | null
+ dst_cidr?: string | null
+ service_object_id?: number | null
+ service_group_id?: number | null
+ log: boolean
+ comment?: string | null
+}
+
+export interface NATRule {
+ id: number
+ name?: string | null
+ priority: number
+ enabled: boolean
+ kind: 'dnat' | 'snat' | 'masquerade'
+ in_zone?: string | null
+ out_zone?: string | null
+ proto?: 'tcp' | 'udp' | 'any' | null
+ match_src_cidr?: string | null
+ match_dst_cidr?: string | null
+ match_dport_start?: number | null
+ match_dport_end?: number | null
+ target_addr?: string | null
+ target_port_start?: number | null
+ target_port_end?: number | null
+ comment?: string | null
+}
+
+export const ZONES: Zone[] = ['any', 'wan', 'lan', 'dmz', 'mgmt', 'cluster']