From 92cf7c1aca63afa3c16a6e6c327772646ac1781e Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Wed, 6 Mar 2019 15:13:27 +0300
Subject: [PATCH] * dhcp: refactor

---
 dhcpd/dhcpd.go | 144 ++++++++++++++++++++++++++++++++-----------------
 1 file changed, 94 insertions(+), 50 deletions(-)

diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go
index d0c61934..f7d99d36 100644
--- a/dhcpd/dhcpd.go
+++ b/dhcpd/dhcpd.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"fmt"
 	"net"
+	"strings"
 	"sync"
 	"time"
 
@@ -59,6 +60,16 @@ type Server struct {
 	sync.RWMutex
 }
 
+// Print information about the available network interfaces
+func printInterfaces() {
+	ifaces, _ := net.Interfaces()
+	var buf strings.Builder
+	for i := range ifaces {
+		buf.WriteString(fmt.Sprintf("\"%s\", ", ifaces[i].Name))
+	}
+	log.Info("Available network interfaces: %s", buf.String())
+}
+
 // Start will listen on port 67 and serve DHCP requests.
 // Even though config can be nil, it is not optional (at least for now), since there are no default values (yet).
 func (s *Server) Start(config *ServerConfig) error {
@@ -69,6 +80,7 @@ func (s *Server) Start(config *ServerConfig) error {
 	iface, err := net.InterfaceByName(s.InterfaceName)
 	if err != nil {
 		s.closeConn() // in case it was already started
+		printInterfaces()
 		return wrapErrPrint(err, "Couldn't find interface by name %s", s.InterfaceName)
 	}
 
@@ -189,6 +201,7 @@ func (s *Server) closeConn() error {
 	return err
 }
 
+// Reserve a lease for the client
 func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
 	// WARNING: do not remove copy()
 	// the given hwaddr by p.CHAddr() in the packet survives only during ServeDHCP() call
@@ -196,11 +209,6 @@ func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
 	hwaddrCOW := p.CHAddr()
 	hwaddr := make(net.HardwareAddr, len(hwaddrCOW))
 	copy(hwaddr, hwaddrCOW)
-	foundLease := s.locateLease(p)
-	if foundLease != nil {
-		// log.Tracef("found lease for %s: %+v", hwaddr, foundLease)
-		return foundLease, nil
-	}
 	// not assigned a lease, create new one, find IP from LRU
 	log.Tracef("Lease not found for %s: creating new one", hwaddr)
 	ip, err := s.findFreeIP(hwaddr)
@@ -216,7 +224,8 @@ func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
 	return lease, nil
 }
 
-func (s *Server) locateLease(p dhcp4.Packet) *Lease {
+// Find a lease for the client
+func (s *Server) findLease(p dhcp4.Packet) *Lease {
 	hwaddr := p.CHAddr()
 	for i := range s.leases {
 		if bytes.Equal([]byte(hwaddr), []byte(s.leases[i].HWAddr)) {
@@ -243,7 +252,6 @@ func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) {
 			// if !bytes.Equal(foundHWaddr, hwaddr) {
 			// 	log.Tracef("SHOULD NOT HAPPEN: hwaddr in IP pool %s is not equal to hwaddr in lease %s", foundHWaddr, hwaddr)
 			// }
-			log.Tracef("will try again")
 			continue
 		}
 		foundIP = newIP
@@ -280,12 +288,7 @@ func (s *Server) unreserveIP(ip net.IP) {
 
 // ServeDHCP handles an incoming DHCP request
 func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) dhcp4.Packet {
-	log.Tracef("Message from client %s: %d", p.CHAddr(), msgType)
-
-	log.Tracef("Leases:")
-	for i, lease := range s.leases {
-		log.Tracef("Lease #%d: hwaddr %s, ip %s, expiry %s", i, lease.HWAddr, lease.IP, lease.Expiry)
-	}
+	s.printLeases()
 
 	switch msgType {
 	case dhcp4.Discover: // Broadcast Packet From Client - Can I have an IP?
@@ -297,26 +300,26 @@ func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dh
 		return s.handleDHCP4Request(p, options)
 
 	case dhcp4.Decline: // Broadcast From Client - Sorry I can't use that IP
-		log.Tracef("Got from client: Decline")
+		return s.handleDecline(p, options)
 
 	case dhcp4.Release: // From Client, I don't need that IP anymore
-		log.Tracef("Got from client: Release")
+		return s.handleRelease(p, options)
 
 	case dhcp4.Inform: // From Client, I have this IP and there's nothing you can do about it
-		log.Tracef("Got from client: Inform")
+		return s.handleInform(p, options)
 
 	// from server -- ignore those but enumerate just in case
 	case dhcp4.Offer: // Broadcast From Server - Here's an IP
-		log.Printf("DHCP: received message from another server: Offer")
+		log.Printf("DHCP: received message from %s: Offer", p.CHAddr())
 
 	case dhcp4.ACK: // From Server, Yes you can have that IP
-		log.Printf("DHCP: received message from another server: ACK")
+		log.Printf("DHCP: received message from %s: ACK", p.CHAddr())
 
 	case dhcp4.NAK: // From Server, No you cannot have that IP
-		log.Printf("DHCP: received message from another server: NAK")
+		log.Printf("DHCP: received message from %s: NAK", p.CHAddr())
 
 	default:
-		log.Printf("DHCP: unknown packet %v from client %s", msgType, p.CHAddr())
+		log.Printf("DHCP: unknown packet %v from %s", msgType, p.CHAddr())
 		return nil
 	}
 	return nil
@@ -324,12 +327,23 @@ func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dh
 
 func (s *Server) handleDiscover(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
 	// find a lease, but don't update lease time
+	var lease *Lease
+	var err error
 
-	lease, err := s.reserveLease(p)
-	if err != nil {
-		log.Tracef("Couldn't find free lease: %s", err)
-		// couldn't find lease, don't respond
-		return nil
+	reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
+	hostname := p.ParseOptions()[dhcp4.OptionHostName]
+	log.Tracef("Message from client: Discover.  ReqIP: %s  HW: %s  Hostname: %s",
+		reqIP, p.CHAddr(), hostname)
+
+	lease = s.findLease(p)
+	for lease == nil {
+		lease, err = s.reserveLease(p)
+		if err != nil {
+			log.Error("Couldn't find free lease: %s", err)
+			return nil
+		}
+
+		break
 	}
 
 	opt := s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])
@@ -339,49 +353,70 @@ func (s *Server) handleDiscover(p dhcp4.Packet, options dhcp4.Options) dhcp4.Pac
 }
 
 func (s *Server) handleDHCP4Request(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
+	var lease *Lease
+	var err error
+
+	reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
+	log.Tracef("Message from client: Request.  IP: %s  ReqIP: %s  HW: %s",
+		p.CIAddr(), reqIP, p.CHAddr())
+
 	server := options[dhcp4.OptionServerIdentifier]
 	if server != nil && !net.IP(server).Equal(s.ipnet.IP) {
 		log.Tracef("Request message not for this DHCP server (%v vs %v)", server, s.ipnet.IP)
 		return nil // Message not for this dhcp server
 	}
 
-	reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
 	if reqIP == nil {
 		reqIP = p.CIAddr()
-	}
 
-	if reqIP.To4() == nil {
-		log.Tracef("Replying with NAK: request IP isn't valid IPv4: %s", reqIP)
+	} else if reqIP == nil || reqIP.To4() == nil {
+		log.Tracef("Requested IP isn't a valid IPv4: %s", reqIP)
 		return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
 	}
 
-	if reqIP.Equal(net.IPv4zero) {
-		log.Tracef("Replying with NAK: request IP is 0.0.0.0")
+	lease = s.findLease(p)
+	if lease == nil {
+		lease, err = s.reserveLease(p)
+		if err != nil {
+			log.Tracef("Couldn't find free lease: %s", err)
+			// couldn't find lease, don't respond
+			return nil
+		}
+	}
+
+	if !lease.IP.Equal(reqIP) {
+		log.Tracef("Lease for %s doesn't match requested/client IP: %s vs %s",
+			lease.HWAddr, lease.IP, reqIP)
 		return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
 	}
 
-	log.Tracef("requested IP is %s", reqIP)
-	lease, err := s.reserveLease(p)
-	if err != nil {
-		log.Tracef("Couldn't find free lease: %s", err)
-		// couldn't find lease, don't respond
-		return nil
-	}
+	lease.Expiry = time.Now().Add(s.leaseTime)
+	log.Tracef("Replying with ACK.  IP: %s  HW: %s  Expire: %s",
+		lease.IP, lease.HWAddr, lease.Expiry)
+	opt := s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])
+	return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, opt)
+}
 
-	if lease.IP.Equal(reqIP) {
-		// IP matches lease IP, nothing else to do
-		lease.Expiry = time.Now().Add(s.leaseTime)
-		log.Tracef("Replying with ACK: request IP matches lease IP, nothing else to do. IP %v for %v", lease.IP, p.CHAddr())
-		opt := s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])
-		return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, opt)
-	}
+func (s *Server) handleInform(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
+	log.Tracef("Message from client: Inform.  IP: %s  HW: %s",
+		p.CIAddr(), p.CHAddr())
 
-	//
-	// requested IP different from lease
-	//
+	return nil
+}
 
-	log.Tracef("lease IP is different from requested IP: %s vs %s", lease.IP, reqIP)
-	return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
+func (s *Server) handleRelease(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
+	log.Tracef("Message from client: Release.  IP: %s  HW: %s",
+		p.CIAddr(), p.CHAddr())
+
+	return nil
+}
+
+func (s *Server) handleDecline(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
+	reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
+	log.Tracef("Message from client: Decline.  IP: %s  HW: %s",
+		reqIP, p.CHAddr())
+
+	return nil
 }
 
 // Leases returns the list of current DHCP leases (thread-safe)
@@ -392,6 +427,15 @@ func (s *Server) Leases() []*Lease {
 	return result
 }
 
+// Print information about the current leases
+func (s *Server) printLeases() {
+	log.Tracef("Leases:")
+	for i, lease := range s.leases {
+		log.Tracef("Lease #%d: hwaddr %s, ip %s, expiry %s",
+			i, lease.HWAddr, lease.IP, lease.Expiry)
+	}
+}
+
 // Reset internal state
 func (s *Server) reset() {
 	s.Lock()