5bb84c159a
- 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
726 regels
18 KiB
Go
726 regels
18 KiB
Go
package dhcp
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
"dhcp-dns-manager/internal/db"
|
|
"dhcp-dns-manager/internal/config"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// DHCP 消息类型
|
|
const (
|
|
MsgDiscover = 1
|
|
MsgOffer = 2
|
|
MsgRequest = 3
|
|
MsgDecline = 4
|
|
MsgACK = 5
|
|
MsgNAK = 6
|
|
MsgRelease = 7
|
|
)
|
|
|
|
// DHCP 选项
|
|
const (
|
|
OptionSubnetMask = 1
|
|
OptionRouter = 3
|
|
OptionDNS = 6
|
|
OptionHostname = 12
|
|
OptionLeaseTime = 51
|
|
OptionMessageType = 53
|
|
OptionServerIdentifier = 54
|
|
OptionRequestedIP = 50
|
|
OptionEnd = 255
|
|
)
|
|
|
|
type Server struct {
|
|
config *config.DHCPConfig
|
|
configReloader func() *config.DHCPConfig // optional: reload config from ConfigManager
|
|
db *db.DB
|
|
leases map[string]*db.DHCPLease
|
|
staticBindings map[string]db.DHCPStaticBinding
|
|
leaseMutex sync.RWMutex
|
|
conn *net.UDPConn
|
|
stopChan chan struct{}
|
|
serverIP net.IP
|
|
usedIPs map[string]string // IP -> MAC
|
|
}
|
|
|
|
func NewServer(cfg *config.DHCPConfig, database *db.DB) *Server {
|
|
return &Server{
|
|
config: cfg,
|
|
db: database,
|
|
leases: make(map[string]*db.DHCPLease),
|
|
staticBindings: make(map[string]db.DHCPStaticBinding),
|
|
usedIPs: make(map[string]string),
|
|
stopChan: make(chan struct{}),
|
|
serverIP: net.ParseIP(cfg.Gateway).To4(),
|
|
}
|
|
}
|
|
|
|
// SetConfigReloader sets a function to reload config dynamically.
|
|
// This allows the DHCP server to pick up config changes made via the web UI.
|
|
func (s *Server) SetConfigReloader(reloader func() *config.DHCPConfig) {
|
|
s.configReloader = reloader
|
|
}
|
|
|
|
// getConfig returns the current config, reloading if a reloader is set.
|
|
func (s *Server) getConfig() *config.DHCPConfig {
|
|
if s.configReloader != nil {
|
|
return s.configReloader()
|
|
}
|
|
return s.config
|
|
}
|
|
|
|
// getServerIP returns the server IP for DHCP options
|
|
func (s *Server) getServerIP() net.IP {
|
|
cfg := s.getConfig()
|
|
return net.ParseIP(cfg.Gateway).To4()
|
|
}
|
|
|
|
func (s *Server) Start() error {
|
|
if !s.config.Enabled {
|
|
return nil
|
|
}
|
|
|
|
// Load existing leases
|
|
s.loadLeases()
|
|
s.loadStaticBindings()
|
|
|
|
// Start lease cleanup goroutine
|
|
go s.cleanupLeases()
|
|
|
|
// Start DHCP server on UDP port 67
|
|
// Use a raw connection to properly handle broadcast responses
|
|
conn, err := newBroadcastUDPConn("0.0.0.0", 67)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to listen on UDP 67: %v", err)
|
|
}
|
|
|
|
s.conn = conn
|
|
log.Printf("DHCP server listening on 0.0.0.0:67")
|
|
|
|
// Handle DHCP requests
|
|
go s.handleDHCP()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) Stop() {
|
|
if s.conn != nil {
|
|
s.conn.Close()
|
|
}
|
|
close(s.stopChan)
|
|
}
|
|
|
|
func (s *Server) handleDHCP() {
|
|
buf := make([]byte, 1024)
|
|
|
|
for {
|
|
select {
|
|
case <-s.stopChan:
|
|
return
|
|
default:
|
|
s.conn.SetReadDeadline(time.Now().Add(1 * time.Second))
|
|
n, remoteAddr, err := s.conn.ReadFromUDP(buf)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
s.processDHCPMessage(buf[:n], remoteAddr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) processDHCPMessage(data []byte, remoteAddr *net.UDPAddr) {
|
|
if len(data) < 240 {
|
|
return
|
|
}
|
|
|
|
// Parse DHCP message
|
|
msgType := parseMessageType(data)
|
|
if msgType == 0 {
|
|
return
|
|
}
|
|
|
|
// Get client MAC
|
|
clientMAC := formatMAC(data[28:34])
|
|
|
|
// Get client IP from packet
|
|
clientIP := net.IP(data[16:20])
|
|
|
|
switch msgType {
|
|
case MsgDiscover:
|
|
s.handleDiscover(data, clientMAC, clientIP, remoteAddr)
|
|
case MsgRequest:
|
|
s.handleRequest(data, clientMAC, clientIP, remoteAddr)
|
|
case MsgRelease:
|
|
s.handleRelease(clientMAC)
|
|
case MsgDecline:
|
|
s.handleDecline(clientMAC)
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleDiscover(data []byte, clientMAC string, clientIP net.IP, remoteAddr *net.UDPAddr) {
|
|
// Find or assign IP
|
|
offeredIP := s.assignIP(clientMAC, data)
|
|
if offeredIP == "" {
|
|
log.Printf("DHCP: No available IP for %s", clientMAC)
|
|
return
|
|
}
|
|
|
|
// Record the offered IP as a provisional lease BEFORE sending the Offer.
|
|
// This ensures that when the client sends Request back, verifyAssignment
|
|
// will find the lease and return true (instead of NAKing the client).
|
|
s.recordLease(clientMAC, offeredIP, data)
|
|
|
|
// Send DHCP Offer
|
|
s.sendOffer(data, clientMAC, offeredIP, remoteAddr)
|
|
log.Printf("DHCP: Offered %s to %s", offeredIP, clientMAC)
|
|
}
|
|
|
|
func (s *Server) handleRequest(data []byte, clientMAC string, clientIP net.IP, remoteAddr *net.UDPAddr) {
|
|
// Get requested IP
|
|
requestedIP := parseRequestedIP(data).String()
|
|
if requestedIP == "<nil>" || requestedIP == "" {
|
|
// Fallback to ciaddr
|
|
requestedIP = clientIP.String()
|
|
}
|
|
|
|
// Verify the requested IP
|
|
if s.verifyAssignment(clientMAC, requestedIP) {
|
|
// Send DHCP ACK
|
|
s.sendACK(data, clientMAC, requestedIP, remoteAddr)
|
|
log.Printf("DHCP: ACK %s to %s", requestedIP, clientMAC)
|
|
|
|
// Record lease
|
|
s.recordLease(clientMAC, requestedIP, data)
|
|
} else {
|
|
// Send DHCP NAK
|
|
s.sendNAK(data, remoteAddr)
|
|
log.Printf("DHCP: NAK for %s (requested %s)", clientMAC, requestedIP)
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleRelease(clientMAC string) {
|
|
s.leaseMutex.Lock()
|
|
defer s.leaseMutex.Unlock()
|
|
|
|
if lease, exists := s.leases[clientMAC]; exists {
|
|
log.Printf("DHCP: Released %s for %s", lease.IP, clientMAC)
|
|
delete(s.leases, clientMAC)
|
|
delete(s.usedIPs, lease.IP)
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleDecline(clientMAC string) {
|
|
s.leaseMutex.Lock()
|
|
defer s.leaseMutex.Unlock()
|
|
|
|
if lease, exists := s.leases[clientMAC]; exists {
|
|
log.Printf("DHCP: Declined %s for %s", lease.IP, clientMAC)
|
|
delete(s.leases, clientMAC)
|
|
delete(s.usedIPs, lease.IP)
|
|
}
|
|
}
|
|
|
|
func (s *Server) assignIP(clientMAC string, data []byte) string {
|
|
s.leaseMutex.Lock()
|
|
defer s.leaseMutex.Unlock()
|
|
|
|
// Check static binding first
|
|
if binding, exists := s.staticBindings[clientMAC]; exists {
|
|
return binding.IP
|
|
}
|
|
|
|
// Check if client already has a lease
|
|
if lease, exists := s.leases[clientMAC]; exists {
|
|
return lease.IP
|
|
}
|
|
|
|
// Find available IP
|
|
startIP := net.ParseIP(s.getConfig().IPPoolStart).To4()
|
|
endIP := net.ParseIP(s.getConfig().IPPoolEnd).To4()
|
|
|
|
if startIP == nil || endIP == nil {
|
|
return ""
|
|
}
|
|
|
|
startInt := uint32(startIP[0])<<24 | uint32(startIP[1])<<16 | uint32(startIP[2])<<8 | uint32(startIP[3])
|
|
endInt := uint32(endIP[0])<<24 | uint32(endIP[1])<<16 | uint32(endIP[2])<<8 | uint32(endIP[3])
|
|
|
|
// Build set of used IPs
|
|
usedIPs := make(map[string]bool)
|
|
for _, lease := range s.leases {
|
|
usedIPs[lease.IP] = true
|
|
}
|
|
|
|
// Find first available IP
|
|
for ip := startInt; ip <= endInt; ip++ {
|
|
ipBytes := []byte{
|
|
byte(ip >> 24),
|
|
byte(ip >> 16),
|
|
byte(ip >> 8),
|
|
byte(ip),
|
|
}
|
|
ipStr := fmt.Sprintf("%d.%d.%d.%d", ipBytes[0], ipBytes[1], ipBytes[2], ipBytes[3])
|
|
|
|
// Skip gateway and excluded IPs
|
|
if ipStr == s.getConfig().Gateway {
|
|
continue
|
|
}
|
|
|
|
excluded := false
|
|
for _, excl := range s.getConfig().ExcludedIPs {
|
|
if ipStr == excl {
|
|
excluded = true
|
|
break
|
|
}
|
|
}
|
|
if excluded {
|
|
continue
|
|
}
|
|
|
|
if !usedIPs[ipStr] {
|
|
return ipStr
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func (s *Server) verifyAssignment(clientMAC, ip string) bool {
|
|
s.leaseMutex.RLock()
|
|
defer s.leaseMutex.RUnlock()
|
|
|
|
// Check static binding
|
|
if binding, exists := s.staticBindings[clientMAC]; exists {
|
|
return binding.IP == ip
|
|
}
|
|
|
|
// Check if IP is assigned to this client
|
|
if lease, exists := s.leases[clientMAC]; exists {
|
|
return lease.IP == ip
|
|
}
|
|
|
|
// IP not in lease map yet — this is a new client that just got an Offer.
|
|
// Verify the IP is within the configured pool range and not already taken.
|
|
startIP := net.ParseIP(s.getConfig().IPPoolStart).To4()
|
|
endIP := net.ParseIP(s.getConfig().IPPoolEnd).To4()
|
|
if startIP == nil || endIP == nil {
|
|
return false
|
|
}
|
|
|
|
clientIP := net.ParseIP(ip).To4()
|
|
if clientIP == nil {
|
|
return false
|
|
}
|
|
|
|
clientInt := uint32(clientIP[0])<<24 | uint32(clientIP[1])<<16 | uint32(clientIP[2])<<8 | uint32(clientIP[3])
|
|
startInt := uint32(startIP[0])<<24 | uint32(startIP[1])<<16 | uint32(startIP[2])<<8 | uint32(startIP[3])
|
|
endInt := uint32(endIP[0])<<24 | uint32(endIP[1])<<16 | uint32(endIP[2])<<8 | uint32(endIP[3])
|
|
|
|
if clientInt < startInt || clientInt > endInt {
|
|
return false
|
|
}
|
|
|
|
// Make sure no other client already has this IP
|
|
for mac, lease := range s.leases {
|
|
if lease.IP == ip && mac != clientMAC {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (s *Server) recordLease(clientMAC, ip string, data []byte) {
|
|
lease := &db.DHCPLease{
|
|
MAC: clientMAC,
|
|
IP: ip,
|
|
ExpiresAt: time.Now().Add(time.Duration(s.getConfig().LeaseTime) * time.Second).Unix(),
|
|
}
|
|
|
|
// Try to get hostname from DHCP options
|
|
if hostname := parseHostname(data); hostname != "" {
|
|
lease.Hostname = hostname
|
|
}
|
|
|
|
s.leaseMutex.Lock()
|
|
// Check if lease already exists (e.g. from Offer phase)
|
|
existingLease, alreadyExists := s.leases[clientMAC]
|
|
s.leases[clientMAC] = lease
|
|
s.usedIPs[ip] = clientMAC
|
|
s.leaseMutex.Unlock()
|
|
|
|
// Save to database — update if already exists to avoid duplicates
|
|
if alreadyExists && existingLease.ID > 0 {
|
|
lease.ID = existingLease.ID
|
|
s.db.Save(lease)
|
|
} else {
|
|
s.db.Create(lease)
|
|
}
|
|
}
|
|
|
|
func (s *Server) sendOffer(data []byte, clientMAC, offeredIP string, remoteAddr *net.UDPAddr) {
|
|
// Build DHCP OFFER message
|
|
response := buildDHCPMessage(MsgOffer, data, offeredIP, s.config)
|
|
|
|
// Add options
|
|
response = appendOption(response, OptionSubnetMask, []byte(net.ParseIP(s.getConfig().Netmask).To4()))
|
|
response = appendOption(response, OptionRouter, []byte(net.ParseIP(s.getConfig().Gateway).To4()))
|
|
|
|
// Add DNS servers
|
|
if len(s.getConfig().DNSServers) > 0 {
|
|
var dnsBytes []byte
|
|
for _, dns := range s.getConfig().DNSServers {
|
|
dnsBytes = append(dnsBytes, net.ParseIP(dns).To4()...)
|
|
}
|
|
response = appendOption(response, OptionDNS, dnsBytes)
|
|
}
|
|
|
|
// Add lease time
|
|
leaseTime := make([]byte, 4)
|
|
binary.BigEndian.PutUint32(leaseTime, uint32(s.getConfig().LeaseTime))
|
|
response = appendOption(response, OptionLeaseTime, leaseTime)
|
|
|
|
// Add server identifier
|
|
response = appendOption(response, OptionServerIdentifier, []byte(s.getServerIP().To4()))
|
|
|
|
// Add end option
|
|
response = append(response, OptionEnd)
|
|
|
|
// Send response — use broadcast if client has no IP yet (ciaddr == 0.0.0.0)
|
|
targetAddr := s.getResponseAddr(data, remoteAddr)
|
|
s.conn.WriteToUDP(response, targetAddr)
|
|
}
|
|
|
|
func (s *Server) sendACK(data []byte, clientMAC, ip string, remoteAddr *net.UDPAddr) {
|
|
response := buildDHCPMessage(MsgACK, data, ip, s.config)
|
|
|
|
// Add options
|
|
response = appendOption(response, OptionSubnetMask, []byte(net.ParseIP(s.getConfig().Netmask).To4()))
|
|
response = appendOption(response, OptionRouter, []byte(net.ParseIP(s.getConfig().Gateway).To4()))
|
|
|
|
if len(s.getConfig().DNSServers) > 0 {
|
|
var dnsBytes []byte
|
|
for _, dns := range s.getConfig().DNSServers {
|
|
dnsBytes = append(dnsBytes, net.ParseIP(dns).To4()...)
|
|
}
|
|
response = appendOption(response, OptionDNS, dnsBytes)
|
|
}
|
|
|
|
leaseTime := make([]byte, 4)
|
|
binary.BigEndian.PutUint32(leaseTime, uint32(s.getConfig().LeaseTime))
|
|
response = appendOption(response, OptionLeaseTime, leaseTime)
|
|
|
|
response = appendOption(response, OptionServerIdentifier, []byte(s.getServerIP().To4()))
|
|
response = append(response, OptionEnd)
|
|
|
|
targetAddr := s.getResponseAddr(data, remoteAddr)
|
|
s.conn.WriteToUDP(response, targetAddr)
|
|
}
|
|
|
|
func (s *Server) sendNAK(data []byte, remoteAddr *net.UDPAddr) {
|
|
response := buildDHCPMessage(MsgNAK, data, "0.0.0.0", s.config)
|
|
response = appendOption(response, OptionServerIdentifier, []byte(s.getServerIP().To4()))
|
|
response = append(response, OptionEnd)
|
|
|
|
// NAK must always be broadcast
|
|
broadcastAddr := &net.UDPAddr{IP: net.IPv4bcast, Port: 68}
|
|
s.conn.WriteToUDP(response, broadcastAddr)
|
|
}
|
|
|
|
// getResponseAddr determines where to send the DHCP response.
|
|
// Per RFC 2131: if ciaddr is 0.0.0.0, broadcast to 255.255.255.255:68.
|
|
// Otherwise unicast to ciaddr:68.
|
|
func (s *Server) getResponseAddr(data []byte, remoteAddr *net.UDPAddr) *net.UDPAddr {
|
|
if len(data) < 28 {
|
|
return remoteAddr
|
|
}
|
|
|
|
// ciaddr is at bytes 24-27
|
|
ciaddr := net.IP(data[24:28])
|
|
if ciaddr.Equal(net.IPv4zero) || ciaddr.IsUnspecified() {
|
|
// Client has no IP yet — broadcast
|
|
return &net.UDPAddr{IP: net.IPv4bcast, Port: 68}
|
|
}
|
|
|
|
// Client already has an IP — unicast
|
|
return &net.UDPAddr{IP: ciaddr, Port: 68}
|
|
}
|
|
|
|
func (s *Server) loadLeases() {
|
|
leases, err := s.db.GetActiveLeases()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
s.leaseMutex.Lock()
|
|
defer s.leaseMutex.Unlock()
|
|
|
|
for _, lease := range leases {
|
|
s.leases[lease.MAC] = &lease
|
|
s.usedIPs[lease.IP] = lease.MAC
|
|
}
|
|
}
|
|
|
|
func (s *Server) loadStaticBindings() {
|
|
bindings, err := s.db.GetStaticBindings()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, binding := range bindings {
|
|
s.staticBindings[binding.MAC] = binding
|
|
}
|
|
}
|
|
|
|
func (s *Server) cleanupLeases() {
|
|
ticker := time.NewTicker(1 * time.Minute)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
s.leaseMutex.Lock()
|
|
now := time.Now().Unix()
|
|
for mac, lease := range s.leases {
|
|
if lease.ExpiresAt < now {
|
|
delete(s.leases, mac)
|
|
delete(s.usedIPs, lease.IP)
|
|
log.Printf("DHCP: Lease expired for %s (%s)", mac, lease.IP)
|
|
}
|
|
}
|
|
s.leaseMutex.Unlock()
|
|
case <-s.stopChan:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) GetLeases() []db.DHCPLease {
|
|
s.leaseMutex.RLock()
|
|
defer s.leaseMutex.RUnlock()
|
|
|
|
leases := make([]db.DHCPLease, 0, len(s.leases))
|
|
for _, lease := range s.leases {
|
|
leases = append(leases, *lease)
|
|
}
|
|
return leases
|
|
}
|
|
|
|
func (s *Server) CreateStaticBinding(mac, ip, hostname, description string) error {
|
|
binding := db.DHCPStaticBinding{
|
|
MAC: mac,
|
|
IP: ip,
|
|
Hostname: hostname,
|
|
Description: description,
|
|
Enabled: true,
|
|
}
|
|
|
|
err := s.db.Create(&binding).Error
|
|
if err == nil {
|
|
s.staticBindings[mac] = binding
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (s *Server) DeleteStaticBinding(id uint) error {
|
|
// Get binding first to remove from cache
|
|
var binding db.DHCPStaticBinding
|
|
s.db.First(&binding, id)
|
|
delete(s.staticBindings, binding.MAC)
|
|
|
|
return s.db.Delete(&db.DHCPStaticBinding{}, id).Error
|
|
}
|
|
|
|
func (s *Server) GetStaticBindings() ([]db.DHCPStaticBinding, error) {
|
|
return s.db.GetStaticBindings()
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func parseMessageType(data []byte) byte {
|
|
// DHCP message type is option 53
|
|
for i := 240; i < len(data)-1; i++ {
|
|
if data[i] == OptionMessageType && i+2 < len(data) {
|
|
return data[i+2]
|
|
}
|
|
if data[i] == OptionEnd {
|
|
break
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func parseRequestedIP(data []byte) net.IP {
|
|
// DHCP requested IP is option 50
|
|
for i := 240; i < len(data)-1; i++ {
|
|
if data[i] == OptionRequestedIP && i+5 < len(data) && data[i+1] == 4 {
|
|
return net.IP(data[i+2 : i+6])
|
|
}
|
|
if data[i] == OptionEnd {
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseHostname(data []byte) string {
|
|
// DHCP hostname is option 12
|
|
for i := 240; i < len(data)-1; i++ {
|
|
if data[i] == OptionHostname && i+2 < len(data) {
|
|
length := int(data[i+1])
|
|
if i+2+length <= len(data) {
|
|
return string(data[i+2 : i+2+length])
|
|
}
|
|
}
|
|
if data[i] == OptionEnd {
|
|
break
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func formatMAC(mac []byte) string {
|
|
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x",
|
|
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5])
|
|
}
|
|
|
|
func buildDHCPMessage(msgType byte, request []byte, ip string, cfg *config.DHCPConfig) []byte {
|
|
response := make([]byte, 240)
|
|
|
|
// Copy operation, hardware type, hardware address length, hops
|
|
response[0] = 2 // BOOTREPLY
|
|
response[1] = request[1] // hardware type
|
|
response[2] = request[2] // hardware address length
|
|
|
|
// Copy transaction ID
|
|
copy(response[4:8], request[4:8])
|
|
|
|
// Set offered IP
|
|
ipBytes := net.ParseIP(ip).To4()
|
|
copy(response[16:20], ipBytes)
|
|
|
|
// Set server IP
|
|
serverIP := net.ParseIP(cfg.Gateway).To4()
|
|
copy(response[128:132], serverIP)
|
|
|
|
// Copy client MAC
|
|
copy(response[28:34], request[28:34])
|
|
|
|
// DHCP magic cookie
|
|
response[236] = 99
|
|
response[237] = 130
|
|
response[238] = 83
|
|
response[239] = 99
|
|
|
|
// Add message type option
|
|
response = append(response, OptionMessageType)
|
|
response = append(response, 1) // length
|
|
response = append(response, msgType)
|
|
|
|
return response
|
|
}
|
|
|
|
func appendOption(data []byte, option byte, value []byte) []byte {
|
|
data = append(data, option)
|
|
data = append(data, byte(len(value)))
|
|
data = append(data, value...)
|
|
return data
|
|
}
|
|
|
|
// IP 地址管理工具函数
|
|
func IPInRange(ip, start, end string) bool {
|
|
ipAddr := net.ParseIP(ip)
|
|
startAddr := net.ParseIP(start)
|
|
endAddr := net.ParseIP(end)
|
|
|
|
if ipAddr == nil || startAddr == nil || endAddr == nil {
|
|
return false
|
|
}
|
|
|
|
ipBytes := ipAddr.To4()
|
|
startBytes := startAddr.To4()
|
|
endBytes := endAddr.To4()
|
|
|
|
if ipBytes == nil || startBytes == nil || endBytes == nil {
|
|
return false
|
|
}
|
|
|
|
ipInt := uint32(ipBytes[0])<<24 | uint32(ipBytes[1])<<16 | uint32(ipBytes[2])<<8 | uint32(ipBytes[3])
|
|
startInt := uint32(startBytes[0])<<24 | uint32(startBytes[1])<<16 | uint32(startBytes[2])<<8 | uint32(startBytes[3])
|
|
endInt := uint32(endBytes[0])<<24 | uint32(endBytes[1])<<16 | uint32(endBytes[2])<<8 | uint32(endBytes[3])
|
|
|
|
return ipInt >= startInt && ipInt <= endInt
|
|
}
|
|
|
|
// newBroadcastUDPConn creates a UDP listener on the given host:port with SO_BROADCAST enabled.
|
|
// This is required for DHCP because responses to clients without an IP must be sent to
|
|
// the broadcast address (255.255.255.255:68).
|
|
func newBroadcastUDPConn(host string, port int) (*net.UDPConn, error) {
|
|
// Create a raw UDP socket so we can set SO_BROADCAST
|
|
sock, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_UDP)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create socket: %v", err)
|
|
}
|
|
|
|
// Enable SO_BROADCAST so we can send to 255.255.255.255
|
|
if err := unix.SetsockoptInt(sock, unix.SOL_SOCKET, unix.SO_BROADCAST, 1); err != nil {
|
|
unix.Close(sock)
|
|
return nil, fmt.Errorf("failed to set SO_BROADCAST: %v", err)
|
|
}
|
|
|
|
// Bind to the address
|
|
sa := &unix.SockaddrInet4{Port: port}
|
|
copy(sa.Addr[:], net.ParseIP(host).To4())
|
|
if err := unix.Bind(sock, sa); err != nil {
|
|
unix.Close(sock)
|
|
return nil, fmt.Errorf("failed to bind: %v", err)
|
|
}
|
|
|
|
// Wrap the raw socket in a net.UDPConn
|
|
file := os.NewFile(uintptr(sock), fmt.Sprintf("udp-%s-%d", host, port))
|
|
conn, err := net.FileConn(file)
|
|
if err != nil {
|
|
unix.Close(sock)
|
|
return nil, fmt.Errorf("failed to wrap socket: %v", err)
|
|
}
|
|
|
|
udpConn, ok := conn.(*net.UDPConn)
|
|
if !ok {
|
|
conn.Close()
|
|
return nil, fmt.Errorf("not a UDP connection")
|
|
}
|
|
|
|
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
|
|
}
|