feat(dns): Listen-Adressen als Multi-Select aus Kernel-IPs
Vorher: Free-Text-Input ('127.0.0.1, ::1, 10.10.20.3') — Operator
musste Werte tippen + auf Format aufpassen.
Jetzt: Multi-Select (mode='tags') das die IPs aus /system/interfaces
+ vier Spezial-Werte (0.0.0.0, ::, 127.0.0.1, ::1) anbietet. Optionen
zeigen IP + Iface-Name + Family ('10.0.20.26 — ens19 (IPv4)'). Tag-
Mode lässt zusätzlich freie Eingabe zu, falls eine geplante VIP noch
nicht im Kernel ist.
Convertierung Form↔Wire: UI Array ↔ DB Comma-CSV.
Version 1.0.35.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,7 +73,7 @@ const NAV: NavSection[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const VERSION = '1.0.34'
|
||||
const VERSION = '1.0.35'
|
||||
|
||||
export default function Sidebar({ isOpen, onClose }: SidebarProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -433,7 +433,9 @@
|
||||
"settings": {
|
||||
"intro": "Globale Resolver-Settings. Änderungen hier reloaden Unbound automatisch.",
|
||||
"listenAddresses": "Listen-Adressen",
|
||||
"listenAddressesExtra": "Komma-separiert. Standard 127.0.0.1+::1 — wenn LAN-Clients fragen sollen, z.B. die LAN-Iface-IP zusätzlich (10.10.20.3).",
|
||||
"listenAddressesPlaceholder": "IPs wählen (oder eintippen)",
|
||||
"listenAddressesRequired": "Mindestens eine Adresse erforderlich.",
|
||||
"listenAddressesExtra": "Mehrfachauswahl aus den IPs die der Kernel kennt. 127.0.0.1 + ::1 = nur lokal; weitere LAN-Iface-IPs (z.B. 10.10.20.3) öffnen den Resolver für LAN-Clients. Eigene IPs lassen sich auch eintippen (Enter).",
|
||||
"listenPort": "Port",
|
||||
"upstreamForwards": "Default-Forwarders",
|
||||
"upstreamForwardsExtra": "Wo geht alles hin was nicht lokal ist. Default 1.1.1.1 + 9.9.9.9.",
|
||||
|
||||
@@ -433,7 +433,9 @@
|
||||
"settings": {
|
||||
"intro": "Global resolver settings. Saves reload Unbound automatically.",
|
||||
"listenAddresses": "Listen addresses",
|
||||
"listenAddressesExtra": "Comma-separated. Default 127.0.0.1+::1 — to let LAN clients query, add the LAN iface IP (e.g. 10.10.20.3).",
|
||||
"listenAddressesPlaceholder": "Pick IPs (or type)",
|
||||
"listenAddressesRequired": "At least one address required.",
|
||||
"listenAddressesExtra": "Multi-select from kernel-discovered IPs. 127.0.0.1 + ::1 = local only; LAN iface IPs (e.g. 10.10.20.3) open the resolver to LAN clients. You can also type custom IPs (Enter).",
|
||||
"listenPort": "Port",
|
||||
"upstreamForwards": "Default forwarders",
|
||||
"upstreamForwardsExtra": "Where everything not local goes. Default 1.1.1.1 + 9.9.9.9.",
|
||||
|
||||
@@ -61,6 +61,16 @@ async function getSettings(): Promise<Settings | null> {
|
||||
return isEnvelope(r.data) ? (r.data.data as Settings) : null
|
||||
}
|
||||
|
||||
interface SystemIface {
|
||||
ifname: string
|
||||
addr_info?: Array<{ family: 'inet' | 'inet6'; local: string; prefixlen: number }>
|
||||
}
|
||||
async function listSystemInterfaces(): Promise<SystemIface[]> {
|
||||
const r = await apiClient.get('/system/interfaces')
|
||||
if (!isEnvelope(r.data)) return []
|
||||
return (r.data.data as { interfaces?: SystemIface[] }).interfaces ?? []
|
||||
}
|
||||
|
||||
export default function DNSPage() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
@@ -326,14 +336,51 @@ function RecordsDrawer({ zone, onClose }: RecordsDrawerProps) {
|
||||
|
||||
// ── Settings tab ──────────────────────────────────────────────
|
||||
|
||||
// Settings-Form-Shape unterscheidet sich vom Wire-Shape: listen_addresses
|
||||
// ist im UI ein Array (Multi-Select), wird beim Save zur Komma-CSV.
|
||||
interface SettingsForm extends Omit<Settings, 'listen_addresses'> {
|
||||
listen_addresses: string[]
|
||||
}
|
||||
|
||||
function SettingsTab() {
|
||||
const { t } = useTranslation()
|
||||
const qc = useQueryClient()
|
||||
const { data, isLoading } = useQuery({ queryKey: ['dns', 'settings'], queryFn: getSettings })
|
||||
const [form] = Form.useForm<Settings>()
|
||||
const { data: sys } = useQuery({ queryKey: ['system', 'interfaces'], queryFn: listSystemInterfaces })
|
||||
const [form] = Form.useForm<SettingsForm>()
|
||||
|
||||
// Multi-Select-Optionen: alle IPv4/IPv6 die der Kernel kennt + Spezial-
|
||||
// Werte (0.0.0.0 = alle IPv4, :: = alle IPv6, 127.0.0.1 / ::1 = lo).
|
||||
// Mode "tags" damit der Operator notfalls auch eine IP eintippen kann
|
||||
// die der Kernel noch nicht meldet (z.B. eine geplante VIP).
|
||||
const ipOptions: { value: string; label: string }[] = []
|
||||
ipOptions.push({ value: '0.0.0.0', label: '0.0.0.0 — alle IPv4-Interfaces' })
|
||||
ipOptions.push({ value: '::', label: ':: — alle IPv6-Interfaces' })
|
||||
ipOptions.push({ value: '127.0.0.1', label: '127.0.0.1 — Loopback IPv4' })
|
||||
ipOptions.push({ value: '::1', label: '::1 — Loopback IPv6' })
|
||||
for (const i of sys ?? []) {
|
||||
if (i.ifname === 'lo') continue
|
||||
for (const a of i.addr_info ?? []) {
|
||||
ipOptions.push({
|
||||
value: a.local,
|
||||
label: `${a.local} — ${i.ifname} (${a.family === 'inet' ? 'IPv4' : 'IPv6'})`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const initial: SettingsForm | undefined = data ? {
|
||||
...data,
|
||||
listen_addresses: data.listen_addresses
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean),
|
||||
} : undefined
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: async (v: Settings) => (await apiClient.put('/dns/settings', v)).data,
|
||||
mutationFn: async (v: SettingsForm) => {
|
||||
const body: Settings = { ...v, listen_addresses: v.listen_addresses.join(', ') }
|
||||
return (await apiClient.put('/dns/settings', body)).data
|
||||
},
|
||||
onSuccess: () => {
|
||||
message.success(t('common.save'))
|
||||
void qc.invalidateQueries({ queryKey: ['dns', 'settings'] })
|
||||
@@ -346,14 +393,24 @@ function SettingsTab() {
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={data ?? undefined}
|
||||
initialValues={initial}
|
||||
onFinish={(v) => save.mutate(v)}
|
||||
style={{ maxWidth: 720 }}
|
||||
>
|
||||
<Alert type="info" showIcon className="mb-12" message={t('dns.settings.intro')} />
|
||||
<Form.Item label={t('dns.settings.listenAddresses')} name="listen_addresses" rules={[{ required: true }]}
|
||||
extra={t('dns.settings.listenAddressesExtra')}>
|
||||
<Input placeholder="127.0.0.1, ::1, 10.10.20.3" />
|
||||
<Form.Item
|
||||
label={t('dns.settings.listenAddresses')}
|
||||
name="listen_addresses"
|
||||
rules={[{ required: true, type: 'array', min: 1, message: t('dns.settings.listenAddressesRequired') }]}
|
||||
extra={t('dns.settings.listenAddressesExtra')}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder={t('dns.settings.listenAddressesPlaceholder')}
|
||||
options={ipOptions}
|
||||
showSearch
|
||||
optionFilterProp="value"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('dns.settings.listenPort')} name="listen_port" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={65535} style={{ width: '100%' }} />
|
||||
|
||||
Reference in New Issue
Block a user