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:
@@ -43,7 +43,7 @@ import (
|
|||||||
wgsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/wireguard"
|
wgsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/wireguard"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.0.34"
|
var version = "1.0.35"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
addr := os.Getenv("EDGEGUARD_API_ADDR")
|
addr := os.Getenv("EDGEGUARD_API_ADDR")
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.0.34"
|
var version = "1.0.35"
|
||||||
|
|
||||||
const usage = `edgeguard-ctl — EdgeGuard CLI
|
const usage = `edgeguard-ctl — EdgeGuard CLI
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.0.34"
|
var version = "1.0.35"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// renewTickInterval — how often we re-evaluate expiring certs.
|
// renewTickInterval — how often we re-evaluate expiring certs.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "edgeguard-management-ui",
|
"name": "edgeguard-management-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.34",
|
"version": "1.0.35",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ const NAV: NavSection[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const VERSION = '1.0.34'
|
const VERSION = '1.0.35'
|
||||||
|
|
||||||
export default function Sidebar({ isOpen, onClose }: SidebarProps) {
|
export default function Sidebar({ isOpen, onClose }: SidebarProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|||||||
@@ -433,7 +433,9 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"intro": "Globale Resolver-Settings. Änderungen hier reloaden Unbound automatisch.",
|
"intro": "Globale Resolver-Settings. Änderungen hier reloaden Unbound automatisch.",
|
||||||
"listenAddresses": "Listen-Adressen",
|
"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",
|
"listenPort": "Port",
|
||||||
"upstreamForwards": "Default-Forwarders",
|
"upstreamForwards": "Default-Forwarders",
|
||||||
"upstreamForwardsExtra": "Wo geht alles hin was nicht lokal ist. Default 1.1.1.1 + 9.9.9.9.",
|
"upstreamForwardsExtra": "Wo geht alles hin was nicht lokal ist. Default 1.1.1.1 + 9.9.9.9.",
|
||||||
|
|||||||
@@ -433,7 +433,9 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"intro": "Global resolver settings. Saves reload Unbound automatically.",
|
"intro": "Global resolver settings. Saves reload Unbound automatically.",
|
||||||
"listenAddresses": "Listen addresses",
|
"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",
|
"listenPort": "Port",
|
||||||
"upstreamForwards": "Default forwarders",
|
"upstreamForwards": "Default forwarders",
|
||||||
"upstreamForwardsExtra": "Where everything not local goes. Default 1.1.1.1 + 9.9.9.9.",
|
"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
|
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() {
|
export default function DNSPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
@@ -326,14 +336,51 @@ function RecordsDrawer({ zone, onClose }: RecordsDrawerProps) {
|
|||||||
|
|
||||||
// ── Settings tab ──────────────────────────────────────────────
|
// ── 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() {
|
function SettingsTab() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const { data, isLoading } = useQuery({ queryKey: ['dns', 'settings'], queryFn: getSettings })
|
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({
|
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: () => {
|
onSuccess: () => {
|
||||||
message.success(t('common.save'))
|
message.success(t('common.save'))
|
||||||
void qc.invalidateQueries({ queryKey: ['dns', 'settings'] })
|
void qc.invalidateQueries({ queryKey: ['dns', 'settings'] })
|
||||||
@@ -346,14 +393,24 @@ function SettingsTab() {
|
|||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
initialValues={data ?? undefined}
|
initialValues={initial}
|
||||||
onFinish={(v) => save.mutate(v)}
|
onFinish={(v) => save.mutate(v)}
|
||||||
style={{ maxWidth: 720 }}
|
style={{ maxWidth: 720 }}
|
||||||
>
|
>
|
||||||
<Alert type="info" showIcon className="mb-12" message={t('dns.settings.intro')} />
|
<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 }]}
|
<Form.Item
|
||||||
extra={t('dns.settings.listenAddressesExtra')}>
|
label={t('dns.settings.listenAddresses')}
|
||||||
<Input placeholder="127.0.0.1, ::1, 10.10.20.3" />
|
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>
|
||||||
<Form.Item label={t('dns.settings.listenPort')} name="listen_port" rules={[{ required: true }]}>
|
<Form.Item label={t('dns.settings.listenPort')} name="listen_port" rules={[{ required: true }]}>
|
||||||
<InputNumber min={1} max={65535} style={{ width: '100%' }} />
|
<InputNumber min={1} max={65535} style={{ width: '100%' }} />
|
||||||
|
|||||||
Reference in New Issue
Block a user