Fix DHCP client unable to get IP and config not persisting
- Fixed verifyAssignment being too strict for new clients - Fixed parseRequestedIP string conversion bug - Fixed response sent to 0.0.0.0 instead of broadcast address - Added SO_BROADCAST support for UDP socket - Fixed session persistence after page refresh (localStorage) - Added in-memory session store for auth middleware - Added config reloader so DHCP server picks up web UI changes dynamically
This commit is contained in:
@@ -0,0 +1,696 @@
|
||||
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
|
||||
}
|
||||
|
||||
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.serverIP.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.serverIP.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.serverIP.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
|
||||
}
|
||||
Reference in New Issue
Block a user