From 9c8560bdd04d3577cd0e546b7b0c2abd338f2aec Mon Sep 17 00:00:00 2001 From: Umegbewe Nwebedu Date: Thu, 16 Jan 2025 11:43:05 +0100 Subject: [PATCH] feat: optional arp checks --- conf.yaml | 1 + internal/config/config.go | 1 + pkg/dhcp/lease.go | 8 ++++---- pkg/dhcp/server.go | 4 ++-- tests/lease_test.go | 22 +++++++++++----------- tests/server_test.go | 6 ++++++ 6 files changed, 25 insertions(+), 17 deletions(-) diff --git a/conf.yaml b/conf.yaml index 7563510..f9734af 100644 --- a/conf.yaml +++ b/conf.yaml @@ -12,6 +12,7 @@ server: interface: "en0" # Network interface to bind the DHCP server to lease_db_path: "leases.db" # Path to the db file for lease persistence cleanup_expired_interval: 120 # Interval in seconds for cleaning up expired leases + arp_check: true # Enable or disable server ARP check for ip conflicts logging: level: "info" # Log level (debug, info, warn, error) diff --git a/internal/config/config.go b/internal/config/config.go index 0944397..7885b5d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -23,6 +23,7 @@ type Config struct { Port int `yaml:"port" default:"67"` LeaseDBPath string `yaml:"lease_db_path"` CleanupExpiredInterval int `yaml:"cleanup_expired_interval" default:"120"` + ARPCheck bool `yaml:"arp_check" default:"true"` } `yaml:"server"` Logging struct { Level string `yaml:"level"` diff --git a/pkg/dhcp/lease.go b/pkg/dhcp/lease.go index 8af91df..fb004ad 100644 --- a/pkg/dhcp/lease.go +++ b/pkg/dhcp/lease.go @@ -107,7 +107,7 @@ func LeasePool(start, end string, leaseTime int, dbPath string) (*Pool, error) { return p, nil } -func (p *Pool) Allocate(iface *net.Interface, mac net.HardwareAddr) (net.IP, error) { +func (p *Pool) Allocate(iface *net.Interface, mac net.HardwareAddr, arp bool) (net.IP, error) { p.mutex.Lock() defer p.mutex.Unlock() @@ -175,7 +175,7 @@ func (p *Pool) Allocate(iface *net.Interface, mac net.HardwareAddr) (net.IP, err ip := offsetToIP(p.start, freeOffset) // loopback interfaces should never do arp resolution - if iface.Name != "lo0" && iface.Name != "lo" { + if arp && iface.Name != "lo0" && iface.Name != "lo" { inUse, err := ARPCheck(iface, ip, 2*time.Second) if err != nil { // ARP check failed, bail out. @@ -392,7 +392,7 @@ func (p *Pool) CleanupExpiredLeases() error { l, de := deserialize(v) if de != nil { - continue // skip corrupt + continue // skip corrupt } if now.After(l.ExpireAt) { if err := c.Delete(); err != nil { @@ -402,7 +402,7 @@ func (p *Pool) CleanupExpiredLeases() error { return err } - // track offset for clearing in bitmap + // track offset for clearing in bitmap offset := int(ipToUint32(l.IP) - startUint) if offset >= 0 && offset < p.totalLeases { freedOffsets = append(freedOffsets, offset) diff --git a/pkg/dhcp/server.go b/pkg/dhcp/server.go index 1454b34..0914511 100644 --- a/pkg/dhcp/server.go +++ b/pkg/dhcp/server.go @@ -145,7 +145,7 @@ func (s *Server) HandleMessage(data []byte, remoteAddr *net.UDPAddr) { func (s *Server) HandleDiscover(message *Message, remoteAddr *net.UDPAddr) { log.Infof("[DHCPDISCOVER] from MAC=%s;", message.CHAddr) - ip, err := s.LeasePool.Allocate(s.Interface, message.CHAddr) + ip, err := s.LeasePool.Allocate(s.Interface, message.CHAddr, s.Config.Server.ARPCheck) if err != nil { log.Errorf("Error allocating lease: %v", err) return @@ -188,7 +188,7 @@ func (s *Server) HandleRequest(message *Message, remoteAddr *net.UDPAddr) { leases := s.LeasePool.GetLeaseByMAC(message.CHAddr) if leases == nil { // client is new or changed MACs. We can attempt new allocation - ip, err := s.LeasePool.Allocate(s.Interface, message.CHAddr) + ip, err := s.LeasePool.Allocate(s.Interface, message.CHAddr, s.Config.Server.ARPCheck) if err != nil { s.SendNAK(message, "No available IP") return diff --git a/tests/lease_test.go b/tests/lease_test.go index b59b0ac..35c1d9a 100644 --- a/tests/lease_test.go +++ b/tests/lease_test.go @@ -20,7 +20,7 @@ func TestLeasePoolBasic(t *testing.T) { defer pool.Close() mac := net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55} - ip, err := pool.Allocate(&net.Interface{Name: "lo"}, mac) + ip, err := pool.Allocate(&net.Interface{Name: "lo"}, mac, false) if err != nil { t.Fatalf("Failed to allocate IP for MAC=%s: %v", mac, err) } @@ -61,14 +61,14 @@ func TestLeasePoolExhaustion(t *testing.T) { {0xDE, 0xAD, 0xBE, 0xEF, 0x03, 0x30}, } for _, mac := range macs { - _, err := pool.Allocate(&net.Interface{Name: "lo"}, mac) + _, err := pool.Allocate(&net.Interface{Name: "lo"}, mac, false) if err != nil { t.Fatalf("Unexpected error allocating IP: %v", err) } } // next allocation should fail - _, err = pool.Allocate(&net.Interface{Name: "lo"}, net.HardwareAddr{0xDE, 0xAD, 0xBE, 0xEF, 0x04, 0x40}) + _, err = pool.Allocate(&net.Interface{Name: "lo"}, net.HardwareAddr{0xDE, 0xAD, 0xBE, 0xEF, 0x04, 0x40}, false) if err == nil { t.Fatal("Expected an error due to IP exhaustion, but got nil") } @@ -85,7 +85,7 @@ func TestLeasePoolExpiry(t *testing.T) { defer pool.Close() mac := net.HardwareAddr{0xCA, 0xFE, 0xBA, 0xBE, 0xBC, 0x01} - ip, err := pool.Allocate(&net.Interface{Name: "lo"}, mac) + ip, err := pool.Allocate(&net.Interface{Name: "lo"}, mac, false) if err != nil { t.Fatalf("Failed to allocate IP=%v", err) } @@ -98,7 +98,7 @@ func TestLeasePoolExpiry(t *testing.T) { } // allocation should return the same IP - ip2, err := pool.Allocate(&net.Interface{Name: "lo"}, mac) + ip2, err := pool.Allocate(&net.Interface{Name: "lo"}, mac, false) if err != nil { t.Fatalf("Failed to re-allocate IP after expiry: %v", err) } @@ -125,7 +125,7 @@ func TestLeaseWrapAround(t *testing.T) { mac4, _ := net.ParseMAC("00:11:22:33:44:04") //(offset 0) - ip1, err := pool.Allocate(iface, mac1) + ip1, err := pool.Allocate(iface, mac1, false) if err != nil { t.Fatalf("Allocate(mac1) failed: %v", err) } @@ -134,7 +134,7 @@ func TestLeaseWrapAround(t *testing.T) { } //(offset 1) - ip2, err := pool.Allocate(iface, mac2) + ip2, err := pool.Allocate(iface, mac2, false) if err != nil { t.Fatalf("Allocate(mac2) failed: %v", err) } @@ -143,7 +143,7 @@ func TestLeaseWrapAround(t *testing.T) { } //(offset 2) - ip3, err := pool.Allocate(iface, mac3) + ip3, err := pool.Allocate(iface, mac3, false) if err != nil { t.Fatalf("Allocate(mac3) failed: %v", err) } @@ -156,7 +156,7 @@ func TestLeaseWrapAround(t *testing.T) { t.Fatalf("Release(ip2) failed: %v", err) } - mac4IP, err := pool.Allocate(iface, mac4) + mac4IP, err := pool.Allocate(iface, mac4, false) if err != nil { t.Fatalf("Allocate(mac4) failed: %v", err) } @@ -165,7 +165,7 @@ func TestLeaseWrapAround(t *testing.T) { } mac5, _ := net.ParseMAC("00:11:22:33:44:05") - ip5, err := pool.Allocate(iface, mac5) + ip5, err := pool.Allocate(iface, mac5, false) if err != nil { t.Fatalf("Allocate(mac5) failed: %v", err) } @@ -174,7 +174,7 @@ func TestLeaseWrapAround(t *testing.T) { } mac6, _ := net.ParseMAC("00:11:22:33:44:06") - ip6, err := pool.Allocate(iface, mac6) + ip6, err := pool.Allocate(iface, mac6, false) if err != nil { t.Fatalf("Allocate(mac6) failed: %v", err) } diff --git a/tests/server_test.go b/tests/server_test.go index 0559020..269b2d5 100644 --- a/tests/server_test.go +++ b/tests/server_test.go @@ -27,6 +27,7 @@ func createTestConfig(dbPath string) *config.Config { Port int `yaml:"port" default:"67"` LeaseDBPath string `yaml:"lease_db_path"` CleanupExpiredInterval int `yaml:"cleanup_expired_interval" default:"120"` + ARPCheck bool `yaml:"arp_check" default:"true"` }{ IPStart: "192.168.100.10", IPEnd: "192.168.100.12", @@ -38,6 +39,7 @@ func createTestConfig(dbPath string) *config.Config { Interface: "lo", Port: 6767, LeaseDBPath: dbPath, + ARPCheck: false, }, } } @@ -161,6 +163,7 @@ func TestRequestFlow(t *testing.T) { Port int `yaml:"port" default:"67"` LeaseDBPath string `yaml:"lease_db_path"` CleanupExpiredInterval int `yaml:"cleanup_expired_interval" default:"120"` + ARPCheck bool `yaml:"arp_check" default:"true"` }{ IPStart: "192.168.100.10", IPEnd: "192.168.100.15", @@ -172,6 +175,7 @@ func TestRequestFlow(t *testing.T) { Interface: "lo", Port: 6767, LeaseDBPath: "test_request_flow.db", + ARPCheck: false, }, } @@ -244,6 +248,7 @@ func TestHighConcurrencyFlood(t *testing.T) { Port int `yaml:"port" default:"67"` LeaseDBPath string `yaml:"lease_db_path"` CleanupExpiredInterval int `yaml:"cleanup_expired_interval" default:"120"` + ARPCheck bool `yaml:"arp_check" default:"true"` }{ IPStart: "192.168.0.10", IPEnd: "192.183.255.0", @@ -255,6 +260,7 @@ func TestHighConcurrencyFlood(t *testing.T) { Interface: "lo", Port: 6767, LeaseDBPath: "test_high_concurrency_flood.db", + ARPCheck: false, }, Logging: struct { Level string `yaml:"level"` }{Level: "error"},