feat(dns): Validierung für DNS-Settings — upstream IPs, ACL-CIDRs, TTL-Kreuzcheck

Backend: validateSettings() prüft vor dem Reload-Trigger ob upstream_forwards
gültige IPs (inkl. @port), access_acl gültige IPs/CIDRs und listen_addresses
gültige IPs sind; cache_max_ttl ≥ cache_min_ttl.
Frontend: Pattern-Validatoren auf upstream_forwards + access_acl; TTL-
Kreuzvalidierung mit dependencies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-29 12:52:28 +02:00
parent 0cbc781d4f
commit 31d3485a2f
8 changed files with 113 additions and 12 deletions

View File

@@ -990,7 +990,10 @@
"flushCacheBtn": "DNS-Cache leeren",
"flushCacheTooltip": "Alle gecachten Einträge verwerfen (unbound-control flush_zone .). Verwenden wenn DNS-Änderungen sofort greifen sollen.",
"flushCacheOk": "DNS-Cache geleert",
"flushCacheFailed": "Cache-Flush fehlgeschlagen"
"flushCacheFailed": "Cache-Flush fehlgeschlagen",
"upstreamForwardsInvalid": "Jeder Forwarder muss eine gültige IP sein (z.B. 1.1.1.1 oder 9.9.9.9)",
"accessACLInvalid": "Jeder Eintrag muss eine gültige IP oder CIDR sein (z.B. 10.0.0.0/8)",
"cacheTTLError": "Cache-Max-TTL muss ≥ Cache-Min-TTL sein"
}
},
"fwd": {

View File

@@ -990,7 +990,10 @@
"flushCacheBtn": "Flush DNS cache",
"flushCacheTooltip": "Discard all cached records (unbound-control flush_zone .). Use after DNS changes have propagated.",
"flushCacheOk": "DNS cache flushed",
"flushCacheFailed": "Flush failed"
"flushCacheFailed": "Flush failed",
"upstreamForwardsInvalid": "Each forwarder must be a valid IP (e.g. 1.1.1.1 or 9.9.9.9)",
"accessACLInvalid": "Each entry must be a valid IP or CIDR (e.g. 10.0.0.0/8 or 192.168.1.0/24)",
"cacheTTLError": "Cache max-TTL must be ≥ cache min-TTL"
}
},
"fwd": {

View File

@@ -524,12 +524,40 @@ function SettingsTab() {
<Form.Item label={t('dns.settings.listenPort')} name="listen_port" rules={[{ required: true }]}>
<InputNumber min={1} max={65535} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label={t('dns.settings.upstreamForwards')} name="upstream_forwards" rules={[{ required: true }]}
extra={t('dns.settings.upstreamForwardsExtra')}>
<Form.Item
label={t('dns.settings.upstreamForwards')}
name="upstream_forwards"
extra={t('dns.settings.upstreamForwardsExtra')}
rules={[
{ required: true },
{
validator(_, val: string) {
if (!val) return Promise.resolve()
const IP = /^(\d{1,3}\.){3}\d{1,3}(@\d{1,5})?$|^[0-9a-fA-F:]+(@\d{1,5})?$/
const bad = val.split(',').map(s => s.trim()).filter(Boolean).find(s => !IP.test(s))
return bad ? Promise.reject(new Error(t('dns.settings.upstreamForwardsInvalid'))) : Promise.resolve()
},
},
]}
>
<Input placeholder="1.1.1.1, 9.9.9.9" />
</Form.Item>
<Form.Item label={t('dns.settings.accessACL')} name="access_acl" rules={[{ required: true }]}
extra={t('dns.settings.accessACLExtra')}>
<Form.Item
label={t('dns.settings.accessACL')}
name="access_acl"
extra={t('dns.settings.accessACLExtra')}
rules={[
{ required: true },
{
validator(_, val: string) {
if (!val) return Promise.resolve()
const IPCIDR = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$|^[0-9a-fA-F:]+(\/\d{1,3})?$/
const bad = val.split(',').map(s => s.trim()).filter(Boolean).find(s => !IPCIDR.test(s))
return bad ? Promise.reject(new Error(t('dns.settings.accessACLInvalid'))) : Promise.resolve()
},
},
]}
>
<Input placeholder="127.0.0.0/8, 10.0.0.0/8" />
</Form.Item>
<Form.Item label={t('dns.settings.dnssec')} name="dnssec" valuePropName="checked">
@@ -539,10 +567,25 @@ function SettingsTab() {
<Switch />
</Form.Item>
<Space>
<Form.Item label={t('dns.settings.cacheMin')} name="cache_min_ttl">
<Form.Item label={t('dns.settings.cacheMin')} name="cache_min_ttl" dependencies={['cache_max_ttl']}>
<InputNumber min={0} style={{ width: 120 }} />
</Form.Item>
<Form.Item label={t('dns.settings.cacheMax')} name="cache_max_ttl">
<Form.Item
label={t('dns.settings.cacheMax')}
name="cache_max_ttl"
dependencies={['cache_min_ttl']}
rules={[
({ getFieldValue }) => ({
validator(_, val) {
const minTTL = getFieldValue('cache_min_ttl') as number | undefined
if (val != null && minTTL != null && val < minTTL) {
return Promise.reject(new Error(t('dns.settings.cacheTTLError')))
}
return Promise.resolve()
},
}),
]}
>
<InputNumber min={60} style={{ width: 120 }} />
</Form.Item>
</Space>