Forráskód Böngészése

feat(dhcp): add network assignment to DHCP server instances with full subnet options bindings

Proxmox Router 1 hete
szülő
commit
a52986d077

+ 64 - 6
internal/dhcp/server_manager.go

@@ -147,8 +147,8 @@ func (sm *ServerManager) saveLocked() error {
 func (sm *ServerManager) applyDnsmasqConfigLocked() error {
 	var sb strings.Builder
 	sb.WriteString("# ==================================================================\n")
-	sb.WriteString("# DebTik DHCP Server Configuration (MikroTik RouterOS style)\n")
-	sb.WriteString("# Auto-generated by DebTik Network Appliance. DO NOT EDIT DIRECTLY.\n")
+	sb.WriteString("# Enterprise DHCP Server Configuration\n")
+	sb.WriteString("# Auto-generated by Network Appliance. DO NOT EDIT DIRECTLY.\n")
 	sb.WriteString("# ==================================================================\n\n")
 
 	sb.WriteString("dhcp-authoritative\n")
@@ -171,8 +171,28 @@ func (sm *ServerManager) applyDnsmasqConfigLocked() error {
 			leaseTime = "12h"
 		}
 
+		// Find assigned network for server
+		var assignedNet *DHCPNetwork
+		if srv.Network != "" {
+			for _, n := range sm.networks {
+				if n.Address == srv.Network || n.ID == srv.Network {
+					copyN := n
+					assignedNet = &copyN
+					break
+				}
+			}
+		}
+
 		// Parse Address Pool range (e.g. 192.168.88.10-192.168.88.254)
 		startIP, endIP, netmask := parseAddressPool(srv.AddressPool)
+		if netmask == "" && assignedNet != nil {
+			if assignedNet.Netmask != "" {
+				netmask = assignedNet.Netmask
+			} else if _, ipNet, err := net.ParseCIDR(assignedNet.Address); err == nil && ipNet != nil {
+				netmask = net.IP(ipNet.Mask).String()
+			}
+		}
+
 		if startIP != "" && endIP != "" {
 			if netmask != "" {
 				sb.WriteString(fmt.Sprintf("dhcp-range=set:%s,interface:%s,%s,%s,%s,%s\n", tag, srv.Interface, startIP, endIP, netmask, leaseTime))
@@ -180,11 +200,48 @@ func (sm *ServerManager) applyDnsmasqConfigLocked() error {
 				sb.WriteString(fmt.Sprintf("dhcp-range=set:%s,interface:%s,%s,%s,%s\n", tag, srv.Interface, startIP, endIP, leaseTime))
 			}
 		}
+
+		// Emit assigned network options specifically for this DHCP server tag
+		if assignedNet != nil {
+			targetTag := fmt.Sprintf("tag:%s,", tag)
+
+			if assignedNet.Gateway != "" {
+				sb.WriteString(fmt.Sprintf("dhcp-option=%soption:router,%s\n", targetTag, assignedNet.Gateway))
+			}
+			if len(assignedNet.DNSServers) > 0 {
+				sb.WriteString(fmt.Sprintf("dhcp-option=%soption:dns-server,%s\n", targetTag, strings.Join(assignedNet.DNSServers, ",")))
+			}
+			if assignedNet.Domain != "" {
+				sb.WriteString(fmt.Sprintf("dhcp-option=%soption:domain-name,%s\n", targetTag, assignedNet.Domain))
+			}
+			if len(assignedNet.NTPServers) > 0 {
+				sb.WriteString(fmt.Sprintf("dhcp-option=%soption:ntp-server,%s\n", targetTag, strings.Join(assignedNet.NTPServers, ",")))
+			}
+			if assignedNet.NextServer != "" {
+				sb.WriteString(fmt.Sprintf("dhcp-option=%soption:tftp-server,%s\n", targetTag, assignedNet.NextServer))
+			}
+			if assignedNet.BootFileName != "" {
+				sb.WriteString(fmt.Sprintf("dhcp-option=%soption:bootfile-name,%s\n", targetTag, assignedNet.BootFileName))
+				if assignedNet.NextServer != "" {
+					sb.WriteString(fmt.Sprintf("dhcp-boot=%s%s,,%s\n", targetTag, assignedNet.BootFileName, assignedNet.NextServer))
+				} else {
+					sb.WriteString(fmt.Sprintf("dhcp-boot=%s%s\n", targetTag, assignedNet.BootFileName))
+				}
+			}
+			if len(assignedNet.WINSServers) > 0 {
+				sb.WriteString(fmt.Sprintf("dhcp-option=%soption:netbios-ns,%s\n", targetTag, strings.Join(assignedNet.WINSServers, ",")))
+			}
+			for _, optID := range assignedNet.DHCPOptions {
+				if opt, exists := sm.options[optID]; exists {
+					sb.WriteString(fmt.Sprintf("dhcp-option=%s%d,%s # %s\n", targetTag, opt.Code, opt.Value, opt.Name))
+				}
+			}
+		}
 		sb.WriteString("\n")
 	}
 
-	// Subnet Networks & Options
-	sb.WriteString("# --- DHCP Networks & Options (/ip dhcp-server network) ---\n")
+	// Subnet Networks & Options (Fallback and IP-matching)
+	sb.WriteString("# --- Subnet Networks Fallbacks & IP matching ---\n")
 	for _, netw := range sm.networks {
 		sb.WriteString(fmt.Sprintf("# Subnet: %s\n", netw.Address))
 		_, ipNet, err := net.ParseCIDR(netw.Address)
@@ -250,8 +307,8 @@ func (sm *ServerManager) applyDnsmasqConfigLocked() error {
 		sb.WriteString("\n")
 	}
 
-	// Static Leases & Reservations (/ip dhcp-server lease)
-	sb.WriteString("# --- Static Leases & Reservations (/ip dhcp-server lease) ---\n")
+	// Static Leases & Reservations
+	sb.WriteString("# --- Static Leases & Reservations ---\n")
 	for _, l := range sm.staticLeases {
 		if l.Disabled {
 			continue
@@ -714,6 +771,7 @@ func (sm *ServerManager) RunSetupWizard(req DHCPServerSetupRequest) error {
 		ID:            srvID,
 		Name:          fmt.Sprintf("dhcp-%s", req.Interface),
 		Interface:     req.Interface,
+		Network:       req.AddressSpace,
 		AddressPool:   req.AddressPool,
 		LeaseTime:     req.LeaseTime,
 		Authoritative: true,

+ 3 - 2
internal/dhcp/server_types.go

@@ -2,15 +2,16 @@ package dhcp
 
 import "time"
 
-// DHCPServerInstance represents an interface-bound DHCP server (/ip dhcp-server)
+// DHCPServerInstance represents an interface-bound DHCP server
 type DHCPServerInstance struct {
 	ID            string `json:"id"`
 	Name          string `json:"name"`           // e.g. "dhcp1", "defconf"
 	Interface     string `json:"interface"`      // e.g. "bridge1", "eth0"
+	Network       string `json:"network"`        // Assigned DHCP Network CIDR or ID e.g. "192.168.88.0/24"
 	LeaseTime     string `json:"lease_time"`     // e.g. "12h", "1d", "30m"
 	AddressPool   string `json:"address_pool"`   // e.g. "192.168.88.10-192.168.88.254"
 	Authoritative bool   `json:"authoritative"`  // default true
-	Disabled      bool   `json:"disabled"`       // MikroTik 'X' flag
+	Disabled      bool   `json:"disabled"`       // 'X' flag
 	Comment       string `json:"comment,omitempty"`
 
 	// Runtime telemetry

+ 49 - 0
internal/web/static/app.js

@@ -12670,6 +12670,7 @@ function renderDHCPServers() {
       <td class="py-2.5 px-3 text-center">${renderMikroTikFlag(s.running, s.disabled)}</td>
       <td class="py-2.5 px-3 font-bold text-white font-mono">${escapeHtml(s.name)}</td>
       <td class="py-2.5 px-3 font-mono text-cyan-400 font-semibold">${escapeHtml(s.interface)}</td>
+      <td class="py-2.5 px-3 font-mono font-semibold">${s.network ? `<span class="px-2 py-0.5 rounded text-[11px] bg-emerald-500/10 text-emerald-300 border border-emerald-500/20 font-bold">${escapeHtml(s.network)}</span>` : '<span class="text-slate-500 italic text-[11px]">Unassigned</span>'}</td>
       <td class="py-2.5 px-3 font-mono text-emerald-400">${escapeHtml(s.address_pool)}</td>
       <td class="py-2.5 px-3 font-mono text-slate-300">${escapeHtml(s.lease_time || '12h')}</td>
       <td class="py-2.5 px-3 font-mono text-slate-300">${s.authoritative ? '<span class="text-emerald-400 font-bold">yes</span>' : '<span class="text-slate-500">no</span>'}</td>
@@ -12800,6 +12801,43 @@ function renderDHCPOptions() {
 }
 
 // ==================== MODALS & ACTIONS: SERVER ====================
+function populateDHCPNetworkSelect(elemId, selectedVal) {
+  const sel = document.getElementById(elemId);
+  if (!sel) return;
+  sel.innerHTML = '<option value="">Select network subnet...</option>';
+  const networks = state.dhcpNetworks || [];
+  networks.forEach(n => {
+    const opt = document.createElement('option');
+    opt.value = n.address;
+    opt.innerText = `${n.address} (Gateway: ${n.gateway || 'None'}${n.comment ? ' - ' + n.comment : ''})`;
+    if (selectedVal && (selectedVal === n.address || selectedVal === n.id)) {
+      opt.selected = true;
+    }
+    sel.appendChild(opt);
+  });
+}
+
+function onDHCPServerNetworkChange() {
+  const netSel = document.getElementById('dhcp-server-modal-network');
+  if (!netSel) return;
+  const netVal = netSel.value;
+  if (!netVal) return;
+  const poolInput = document.getElementById('dhcp-server-modal-pool');
+  if (!poolInput) return;
+
+  // Auto-calculate suggested address pool based on subnet CIDR
+  const parts = netVal.split('/');
+  const ip = parts[0];
+  const cidr = parseInt(parts[1] || '24', 10);
+  if (cidr === 24) {
+    const octets = ip.split('.');
+    if (octets.length === 4) {
+      const base = `${octets[0]}.${octets[1]}.${octets[2]}`;
+      poolInput.value = `${base}.10-${base}.254`;
+    }
+  }
+}
+
 function openAddDHCPServerModal() {
   document.getElementById('dhcp-server-modal-title').innerText = 'Add DHCP Server';
   document.getElementById('dhcp-server-modal-id').value = '';
@@ -12811,6 +12849,14 @@ function openAddDHCPServerModal() {
   document.getElementById('dhcp-server-modal-disabled').checked = false;
 
   populateDHCPIfaceSelect('dhcp-server-modal-iface', '');
+  
+  const networks = state.dhcpNetworks || [];
+  const defaultNet = networks.length > 0 ? networks[0].address : '';
+  populateDHCPNetworkSelect('dhcp-server-modal-network', defaultNet);
+  if (defaultNet) {
+    onDHCPServerNetworkChange();
+  }
+
   document.getElementById('dhcp-server-instance-modal').classList.remove('hidden');
 }
 
@@ -12827,6 +12873,7 @@ function openEditDHCPServerModal(id) {
   document.getElementById('dhcp-server-modal-disabled').checked = !!s.disabled;
 
   populateDHCPIfaceSelect('dhcp-server-modal-iface', s.interface);
+  populateDHCPNetworkSelect('dhcp-server-modal-network', s.network || '');
   document.getElementById('dhcp-server-instance-modal').classList.remove('hidden');
 }
 
@@ -12839,6 +12886,7 @@ async function submitSaveDHCPServer(e) {
   const id = document.getElementById('dhcp-server-modal-id').value;
   const name = document.getElementById('dhcp-server-modal-name').value.trim();
   const iface = document.getElementById('dhcp-server-modal-iface').value;
+  const network = document.getElementById('dhcp-server-modal-network').value.trim();
   const pool = document.getElementById('dhcp-server-modal-pool').value.trim();
   const leaseTime = document.getElementById('dhcp-server-modal-leasetime').value.trim() || '12h';
   const comment = document.getElementById('dhcp-server-modal-comment').value.trim();
@@ -12853,6 +12901,7 @@ async function submitSaveDHCPServer(e) {
         id,
         name,
         interface: iface,
+        network,
         address_pool: pool,
         lease_time: leaseTime,
         authoritative,

+ 16 - 6
internal/web/static/index.html

@@ -771,6 +771,7 @@
                       <th class="py-2.5 px-3 w-12 text-center">Flag</th>
                       <th class="py-2.5 px-3">Name</th>
                       <th class="py-2.5 px-3">Interface</th>
+                      <th class="py-2.5 px-3">Assigned Network</th>
                       <th class="py-2.5 px-3">Address Pool</th>
                       <th class="py-2.5 px-3">Lease Time</th>
                       <th class="py-2.5 px-3">Authoritative</th>
@@ -9612,21 +9613,30 @@
         </div>
 
         <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Assigned Network (Subnet) *</label>
+            <select id="dhcp-server-modal-network" onchange="onDHCPServerNetworkChange()" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+              <option value="">Select network subnet...</option>
+            </select>
+            <span class="text-[10px] text-slate-400">Distributes Gateway, DNS, NTP & Options</span>
+          </div>
           <div>
             <label class="block font-semibold text-slate-300 mb-1.5">Address Pool (Range) *</label>
             <input type="text" id="dhcp-server-modal-pool" placeholder="192.168.88.10-192.168.88.254" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
             <span class="text-[10px] text-slate-400">Format: startIP-endIP</span>
           </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4">
           <div>
             <label class="block font-semibold text-slate-300 mb-1.5">Lease Time</label>
             <input type="text" id="dhcp-server-modal-leasetime" value="12h" placeholder="12h, 1d, 30m" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
             <span class="text-[10px] text-slate-400">Default: 12h</span>
           </div>
-        </div>
-
-        <div>
-          <label class="block font-semibold text-slate-300 mb-1.5">Comment</label>
-          <input type="text" id="dhcp-server-modal-comment" placeholder="LAN Office Pool" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white focus:outline-none focus:border-emerald-500">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Comment</label>
+            <input type="text" id="dhcp-server-modal-comment" placeholder="LAN Office Pool" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white focus:outline-none focus:border-emerald-500">
+          </div>
         </div>
 
         <div class="flex items-center justify-between pt-2 border-t border-slate-800/80">
@@ -10159,6 +10169,6 @@
   </div>
 
   <datalist id="fw-addrlist-datalist"></datalist>
-  <script src="app.js?v=10.0"></script>
+  <script src="app.js?v=11.0"></script>
 </body>
 </html>