feat: HA-Cluster v1.2.x — Split-Brain, TOTP, Enterprise-FW, Drift-Fix, VIP-Recovery

- keepalived: pg_role='standby' hat Vorrang vor role für BACKUP-Bestimmung
- keepalived-master.sh: gecrasht Dienste beim MASTER-Übergang starten (nicht nur reload)
- confighash: ip_addresses per Interface-Name hashen statt per FK (Cross-Node-Drift-Fix)
- TOTP/2FA: RFC 6238 — Setup-Flow, QR-Code, Admin-Disable; two-step Login
- Firewall-UI: Enterprise-Design — auto-Beschreibung, icon-only Actions, zero-hit Indikator
- fe80-Filter: Link-local IPv6 aus NTP/DNS Listen-Dropdowns entfernen
- VIP-Dashboard, Dual-Path VRRP, GW-Tracking (Migrations 0033/0034)
- Forward Proxy + DNS erweiterte Einstellungen (Migrations 0031/0032)
- unbound-control: edgeguard in unbound-Gruppe via postinst

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-31 18:18:31 +02:00
parent 49899e984c
commit 1d06b28064
55 changed files with 3132 additions and 672 deletions

View File

@@ -1,6 +1,6 @@
import { useMemo, useState } from 'react'
import {
Alert, Button, Card, Col, Form, Input, InputNumber, Modal, Progress, Row, Select, Space, Statistic, Switch, Tag, Tooltip, Typography, message,
Alert, Button, Card, Col, Form, Input, InputNumber, Modal, Progress, Row, Select, Space, Statistic, Switch, Tag, Tooltip, Typography, message, Divider,
} from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { ArrowDownOutlined, ArrowUpOutlined, BarChartOutlined, CheckCircleOutlined, CloseCircleOutlined, CloudServerOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'
@@ -31,6 +31,18 @@ interface ACL {
interface ServiceStatus { label: string; unit: string; active: boolean; state: string }
interface ProxySettings {
id: number
listen_addresses: string
listen_port: number
cache_mem_mb: number
cache_dir_mb: number
max_obj_size_mb: number
connect_timeout: number
read_timeout: number
request_timeout: number
}
interface FormValues {
name: string
acl_type: string
@@ -98,6 +110,27 @@ export default function ForwardProxyPage() {
refetchInterval: 30_000,
})
const { data: settings, isLoading: settingsLoading } = useQuery({
queryKey: ['fwd-proxy', 'settings'],
queryFn: async () => {
const r = await apiClient.get('/forward-proxy/settings')
if (!isEnvelope(r.data)) return null
return r.data.data as ProxySettings
},
})
const [settingsForm] = Form.useForm<ProxySettings>()
const saveSettings = useMutation({
mutationFn: async (v: ProxySettings) => {
await apiClient.put('/forward-proxy/settings', v)
},
onSuccess: () => {
message.success(t('common.save'))
void qc.invalidateQueries({ queryKey: ['fwd-proxy', 'settings'] })
},
onError: () => message.error(t('fwd.settings.saveFailed')),
})
const [editing, setEditing] = useState<ACL | null>(null)
const [creating, setCreating] = useState(false)
const [form] = Form.useForm<FormValues>()
@@ -284,6 +317,97 @@ export default function ForwardProxyPage() {
</Card>
)}
<Card
size="small"
className="mb-12"
title={t('fwd.settings.title')}
loading={settingsLoading}
>
<Form
form={settingsForm}
layout="vertical"
initialValues={settings ?? {
listen_addresses: '', listen_port: 3128,
cache_mem_mb: 64, cache_dir_mb: 100, max_obj_size_mb: 4,
connect_timeout: 60, read_timeout: 300, request_timeout: 300,
}}
key={settings?.id ?? 'loading'}
onFinish={(v) => saveSettings.mutate(v)}
>
<Row gutter={16}>
<Col xs={24} md={16}>
<Form.Item
label={t('fwd.settings.listenAddresses')}
name="listen_addresses"
extra={t('fwd.settings.listenAddressesExtra')}
>
<Input placeholder="10.0.5.1, 10.0.20.1" disabled={isViewer} />
</Form.Item>
</Col>
<Col xs={24} md={8}>
<Form.Item
label={t('fwd.settings.listenPort')}
name="listen_port"
extra={t('fwd.settings.listenPortExtra')}
>
<InputNumber min={1} max={65535} style={{ width: '100%' }} disabled={isViewer} />
</Form.Item>
</Col>
</Row>
<Divider plain>{t('fwd.settings.cacheSection')}</Divider>
<Row gutter={16}>
<Col xs={24} sm={8}>
<Form.Item label={t('fwd.settings.cacheMemMB')} name="cache_mem_mb" extra={t('fwd.settings.cacheMemMBExtra')}>
<InputNumber min={16} max={8192} style={{ width: '100%' }} addonAfter="MB" disabled={isViewer} />
</Form.Item>
</Col>
<Col xs={24} sm={8}>
<Form.Item label={t('fwd.settings.cacheDirMB')} name="cache_dir_mb" extra={t('fwd.settings.cacheDirMBExtra')}>
<InputNumber min={100} max={102400} style={{ width: '100%' }} addonAfter="MB" disabled={isViewer} />
</Form.Item>
</Col>
<Col xs={24} sm={8}>
<Form.Item label={t('fwd.settings.maxObjSizeMB')} name="max_obj_size_mb" extra={t('fwd.settings.maxObjSizeMBExtra')}>
<InputNumber min={1} max={1024} style={{ width: '100%' }} addonAfter="MB" disabled={isViewer} />
</Form.Item>
</Col>
</Row>
<Divider plain>{t('fwd.settings.timeoutSection')}</Divider>
<Row gutter={16}>
<Col xs={24} sm={8}>
<Form.Item label={t('fwd.settings.connectTimeout')} name="connect_timeout" extra={t('fwd.settings.connectTimeoutExtra')}>
<InputNumber min={5} max={3600} style={{ width: '100%' }} addonAfter="s" disabled={isViewer} />
</Form.Item>
</Col>
<Col xs={24} sm={8}>
<Form.Item label={t('fwd.settings.readTimeout')} name="read_timeout" extra={t('fwd.settings.readTimeoutExtra')}>
<InputNumber min={30} max={3600} style={{ width: '100%' }} addonAfter="s" disabled={isViewer} />
</Form.Item>
</Col>
<Col xs={24} sm={8}>
<Form.Item label={t('fwd.settings.requestTimeout')} name="request_timeout" extra={t('fwd.settings.requestTimeoutExtra')}>
<InputNumber min={30} max={3600} style={{ width: '100%' }} addonAfter="s" disabled={isViewer} />
</Form.Item>
</Col>
</Row>
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
<Button
type="primary"
htmlType="submit"
loading={saveSettings.isPending}
disabled={isViewer}
>
{t('common.save')}
</Button>
</Tooltip>
</Form>
</Card>
<Divider />
<DataTable
rowKey="id"
loading={isLoading}