Add client eviction feature
- Add EvictClient method to DHCP Server - Add /api/dhcp/leases/evict endpoint to force client IP release - Add 'Evict' button in Web UI for online clients - Update table layout to include Action column - Evicted client will be forced to get a new IP on next DHCP request
This commit is contained in:
@@ -700,3 +700,26 @@ func newBroadcastUDPConn(host string, port int) (*net.UDPConn, error) {
|
|||||||
|
|
||||||
return udpConn, nil
|
return udpConn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EvictClient removes a client's lease, forcing them to get a new IP
|
||||||
|
func (s *Server) EvictClient(mac string) error {
|
||||||
|
s.leaseMutex.Lock()
|
||||||
|
defer s.leaseMutex.Unlock()
|
||||||
|
|
||||||
|
lease, exists := s.leases[mac]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("lease not found for MAC %s", mac)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from in-memory lease map
|
||||||
|
delete(s.leases, mac)
|
||||||
|
delete(s.usedIPs, lease.IP)
|
||||||
|
|
||||||
|
// Delete from database
|
||||||
|
if err := s.db.Where("mac = ?", mac).Delete(&db.DHCPLease{}).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to delete lease from database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("DHCP: Evicted client %s (%s)", mac, lease.IP)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ func (s *Server) setupRoutes() {
|
|||||||
protected.GET("/dhcp/bindings", s.handleGetBindings)
|
protected.GET("/dhcp/bindings", s.handleGetBindings)
|
||||||
protected.POST("/dhcp/bindings", s.handleCreateBinding)
|
protected.POST("/dhcp/bindings", s.handleCreateBinding)
|
||||||
protected.DELETE("/dhcp/bindings/:id", s.handleDeleteBinding)
|
protected.DELETE("/dhcp/bindings/:id", s.handleDeleteBinding)
|
||||||
|
protected.POST("/dhcp/leases/evict", s.handleEvictClient)
|
||||||
|
|
||||||
// DNS
|
// DNS
|
||||||
protected.GET("/dns/config", s.handleGetDNSConfig)
|
protected.GET("/dns/config", s.handleGetDNSConfig)
|
||||||
@@ -282,6 +283,30 @@ func (s *Server) handleDeleteBinding(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"message": "Binding deleted"})
|
c.JSON(http.StatusOK, gin.H{"message": "Binding deleted"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleEvictClient(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
MAC string `json:"mac"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.MAC == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "MAC is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.dhcpServer.EvictClient(req.MAC); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Client evicted successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleGetRecords(c *gin.Context) {
|
func (s *Server) handleGetRecords(c *gin.Context) {
|
||||||
records, err := s.dnsServer.GetDNSRecords()
|
records, err := s.dnsServer.GetDNSRecords()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -185,6 +185,17 @@ async function loadClients() {
|
|||||||
statusCell.innerHTML = isActive ? '<span class="status-active">● 在线</span>' : '<span class="status-expired">● 已过期</span>';
|
statusCell.innerHTML = isActive ? '<span class="status-active">● 在线</span>' : '<span class="status-expired">● 已过期</span>';
|
||||||
row.appendChild(statusCell);
|
row.appendChild(statusCell);
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
const actionCell = document.createElement('td');
|
||||||
|
if (isActive) {
|
||||||
|
const evictBtn = document.createElement('button');
|
||||||
|
evictBtn.textContent = '剔除';
|
||||||
|
evictBtn.style.cssText = 'background-color: #f39c12; color: white; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer; margin-right: 5px;';
|
||||||
|
evictBtn.onclick = () => evictClient(lease.MAC, lease.IP);
|
||||||
|
actionCell.appendChild(evictBtn);
|
||||||
|
}
|
||||||
|
row.appendChild(actionCell);
|
||||||
|
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -502,6 +513,28 @@ async function deleteBinding(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function evictClient(mac, ip) {
|
||||||
|
if (!confirm(`确定要剔除主机 ${mac} (${ip}) 吗?\n\n主机将被迫释放该 IP 并重新获取。`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/dhcp/leases/evict`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-Session-ID': sessionId, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ mac, ip })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert('主机已剔除,将重新获取 IP');
|
||||||
|
loadClients();
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
alert('剔除失败:' + (data.error || '未知错误'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('剔除失败:' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load DNS Config
|
// Load DNS Config
|
||||||
async function loadDNSConfig() {
|
async function loadDNSConfig() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -182,11 +182,12 @@
|
|||||||
<th>租约剩余</th>
|
<th>租约剩余</th>
|
||||||
<th>过期时间</th>
|
<th>过期时间</th>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" style="text-align:center;color:#999;">暂无客户端</td>
|
<td colspan="7" style="text-align:center;color:#999;">暂无客户端</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
Reference in New Issue
Block a user