diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go index c6732c7c..7c5ac943 100644 --- a/dhcpd/dhcpd.go +++ b/dhcpd/dhcpd.go @@ -97,8 +97,8 @@ func (s *Server) CheckConfig(config ServerConfig) error { // Create - create object func Create(config ServerConfig) *Server { s := Server{} - s.conf.Conf4.notify = s.onNotify - s.conf.Conf6.notify = s.onNotify + config.Conf4.notify = s.onNotify + config.Conf6.notify = s.onNotify s.conf.DBFilePath = filepath.Join(config.WorkDir, dbFilename) if !webHandlersRegistered && s.conf.HTTPRegister != nil { @@ -135,11 +135,6 @@ func (s *Server) onNotify(flags uint32) { s.notify(int(flags)) } -// Init checks the configuration and initializes the server -func (s *Server) Init(config ServerConfig) error { - return nil -} - // SetOnLeaseChanged - set callback func (s *Server) SetOnLeaseChanged(onLeaseChanged onLeaseChangedT) { s.onLeaseChanged = onLeaseChanged diff --git a/dhcpd/v4.go b/dhcpd/v4.go index 821459fe..d41e94a2 100644 --- a/dhcpd/v4.go +++ b/dhcpd/v4.go @@ -2,6 +2,7 @@ package dhcpd import ( "bytes" + "encoding/binary" "fmt" "net" "sync" @@ -18,8 +19,7 @@ type V4Server struct { srv *server4.Server leasesLock sync.Mutex leases []*Lease - // IP address pool -- if entry is in the pool, then it's attached to a lease - IPpool map[[4]byte]net.HardwareAddr + ipAddrs [256]byte conf V4ServerConf } @@ -39,7 +39,7 @@ type V4ServerConf struct { ICMPTimeout uint32 `json:"icmp_timeout_msec" yaml:"icmp_timeout_msec"` ipStart net.IP - ipStop net.IP + ipEnd net.IP leaseTime time.Duration dnsIPAddrs []net.IP // IPv4 addresses to return to DHCP clients as DNS server addresses routerIP net.IP @@ -53,30 +53,28 @@ func (s *V4Server) WriteDiskConfig(c *V4ServerConf) { *c = s.conf } -func ipInRange(start, stop, ip net.IP) bool { - if len(start) != len(stop) || - len(start) != len(ip) { - return false - } - // return dhcp4.IPInRange(start, stop, ip) - return false +// Return TRUE if IP address is within range [start..stop] +func ipInRange(start net.IP, stop net.IP, ip net.IP) bool { + from := binary.BigEndian.Uint32(start) + to := binary.BigEndian.Uint32(stop) + check := binary.BigEndian.Uint32(ip) + return from <= check && check <= to } // ResetLeases - reset leases -func (s *V4Server) ResetLeases(ll []*Lease) { +func (s *V4Server) ResetLeases(leases []*Lease) { s.leases = nil - s.IPpool = make(map[[4]byte]net.HardwareAddr) - for _, l := range ll { + + for _, l := range leases { if l.Expiry.Unix() != leaseExpireStatic && - !ipInRange(s.conf.ipStart, s.conf.ipStop, l.IP) { + !ipInRange(s.conf.ipStart, s.conf.ipEnd, l.IP) { - log.Tracef("DHCPv4: skipping a lease with IP %v: not within current IP range", l.IP) + log.Debug("DHCPv4: skipping a lease with IP %v: not within current IP range", l.IP) continue } - s.leases = append(s.leases, l) - s.reserveIP(l.IP, l.HWAddr) + s.addLease(l) } } @@ -84,6 +82,7 @@ func (s *V4Server) ResetLeases(ll []*Lease) { func (s *V4Server) GetLeases(flags int) []Lease { var result []Lease now := time.Now().Unix() + s.leasesLock.Lock() for _, lease := range s.leases { if ((flags&LeasesDynamic) != 0 && lease.Expiry.Unix() > now) || @@ -92,6 +91,7 @@ func (s *V4Server) GetLeases(flags int) []Lease { } } s.leasesLock.Unlock() + return result } @@ -118,118 +118,99 @@ func (s *V4Server) FindMACbyIP4(ip net.IP) net.HardwareAddr { return nil } -func (s *V4Server) reserveIP(ip net.IP, hwaddr net.HardwareAddr) { - rawIP := []byte(ip) - IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]} - s.IPpool[IP4] = hwaddr -} - -func (s *V4Server) unreserveIP(ip net.IP) { - rawIP := []byte(ip) - IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]} - delete(s.IPpool, IP4) -} - -func (s *V4Server) findReservedHWaddr(ip net.IP) net.HardwareAddr { - rawIP := []byte(ip) - IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]} - return s.IPpool[IP4] -} - // Add the specified IP to the black list for a time period func (s *V4Server) blacklistLease(lease *Lease) { hw := make(net.HardwareAddr, 6) - s.leasesLock.Lock() - s.reserveIP(lease.IP, hw) lease.HWAddr = hw lease.Hostname = "" lease.Expiry = time.Now().Add(s.conf.leaseTime) - s.conf.notify(LeaseChangedDBStore) - s.leasesLock.Unlock() - s.conf.notify(LeaseChangedBlacklisted) } -// Remove a dynamic lease by IP address -func (s *V4Server) rmDynamicLeaseWithIP(ip net.IP) error { - var newLeases []*Lease - for _, lease := range s.leases { - if net.IP.Equal(lease.IP.To4(), ip) { - if lease.Expiry.Unix() == leaseExpireStatic { - return fmt.Errorf("static lease with the same IP already exists") - } - continue - } - newLeases = append(newLeases, lease) +// Remove (swap) lease by index +func (s *V4Server) leaseRemoveSwapByIndex(i int) { + s.ipAddrs[s.leases[i].IP[3]] = 0 + log.Debug("DHCPv4: removed lease %s", s.leases[i].HWAddr) + + n := len(s.leases) + if i != n-1 { + s.leases[i] = s.leases[n-1] // swap with the last element + } + s.leases = s.leases[:n-1] +} + +// Remove a dynamic lease with the same properties +// Return error if a static lease is found +func (s *V4Server) rmDynamicLease(lease Lease) error { + for i := 0; i < len(s.leases); i++ { + l := s.leases[i] + + if bytes.Equal(l.HWAddr, lease.HWAddr) { + + if l.Expiry.Unix() == leaseExpireStatic { + return fmt.Errorf("static lease already exists") + } + + s.leaseRemoveSwapByIndex(i) + l = s.leases[i] + } + + if bytes.Equal(l.IP, lease.IP) { + + if l.Expiry.Unix() == leaseExpireStatic { + return fmt.Errorf("static lease already exists") + } + + s.leaseRemoveSwapByIndex(i) + } } - s.leases = newLeases - s.unreserveIP(ip) return nil } -// Remove a dynamic lease by IP address -func (s *V4Server) rmDynamicLeaseWithMAC(mac net.HardwareAddr) error { - var newLeases []*Lease - for _, lease := range s.leases { - if bytes.Equal(lease.HWAddr, mac) { - if lease.Expiry.Unix() == leaseExpireStatic { - return fmt.Errorf("static lease with the same IP already exists") - } - s.unreserveIP(lease.IP) - continue - } - newLeases = append(newLeases, lease) - } - s.leases = newLeases - return nil +// Add a lease +func (s *V4Server) addLease(l *Lease) { + s.leases = append(s.leases, l) + s.ipAddrs[l.IP[3]] = 1 + log.Debug("DHCPv4: added lease %s <-> %s", l.IP, l.HWAddr) } -// Remove a lease -func (s *V4Server) rmLease(l Lease) error { - var newLeases []*Lease - for _, lease := range s.leases { - if net.IP.Equal(lease.IP.To4(), l.IP) { - if !bytes.Equal(lease.HWAddr, l.HWAddr) || - lease.Hostname != l.Hostname { +// Remove a lease with the same properies +func (s *V4Server) rmLease(lease Lease) error { + for i, l := range s.leases { + if bytes.Equal(l.IP, lease.IP) { + + if !bytes.Equal(l.HWAddr, lease.HWAddr) || + l.Hostname != lease.Hostname { + return fmt.Errorf("Lease not found") } - continue + + s.leaseRemoveSwapByIndex(i) + return nil } - newLeases = append(newLeases, lease) } - s.leases = newLeases - s.unreserveIP(l.IP) - return nil + return fmt.Errorf("lease not found") } // AddStaticLease adds a static lease (thread-safe) -func (s *V4Server) AddStaticLease(l Lease) error { - if len(l.IP) != 4 { +func (s *V4Server) AddStaticLease(lease Lease) error { + if len(lease.IP) != 4 { return fmt.Errorf("invalid IP") } - if len(l.HWAddr) != 6 { + if len(lease.HWAddr) != 6 { return fmt.Errorf("invalid MAC") } - l.Expiry = time.Unix(leaseExpireStatic, 0) + lease.Expiry = time.Unix(leaseExpireStatic, 0) s.leasesLock.Lock() - - if s.findReservedHWaddr(l.IP) != nil { - err := s.rmDynamicLeaseWithIP(l.IP) - if err != nil { - s.leasesLock.Unlock() - return err - } - } else { - err := s.rmDynamicLeaseWithMAC(l.HWAddr) - if err != nil { - s.leasesLock.Unlock() - return err - } + err := s.rmDynamicLease(lease) + if err != nil { + s.leasesLock.Unlock() + return err } - s.leases = append(s.leases, &l) - s.reserveIP(l.IP, l.HWAddr) + s.addLease(&lease) s.conf.notify(LeaseChangedDBStore) s.leasesLock.Unlock() + s.conf.notify(LeaseChangedAddedStatic) return nil } @@ -244,12 +225,6 @@ func (s *V4Server) RemoveStaticLease(l Lease) error { } s.leasesLock.Lock() - - if s.findReservedHWaddr(l.IP) == nil { - s.leasesLock.Unlock() - return fmt.Errorf("lease not found") - } - err := s.rmLease(l) if err != nil { s.leasesLock.Unlock() @@ -257,6 +232,7 @@ func (s *V4Server) RemoveStaticLease(l Lease) error { } s.conf.notify(LeaseChangedDBStore) s.leasesLock.Unlock() + s.conf.notify(LeaseChangedRemovedStatic) return nil } @@ -280,85 +256,204 @@ func (s *V4Server) addrAvailable(target net.IP) bool { pinger.Count = 1 reply := false pinger.OnRecv = func(pkt *ping.Packet) { - // log.Tracef("Received ICMP Reply from %v", target) reply = true } - log.Tracef("Sending ICMP Echo to %v", target) + log.Debug("DHCPv4: Sending ICMP Echo to %v", target) pinger.Run() if reply { - log.Info("DHCP: IP conflict: %v is already used by another device", target) + log.Info("DHCPv4: IP conflict: %v is already used by another device", target) return false } - log.Tracef("ICMP procedure is complete: %v", target) + log.Debug("DHCPv4: ICMP procedure is complete: %v", target) return true } +// Find lease by MAC func (s *V4Server) findLease(mac net.HardwareAddr) *Lease { + for i := range s.leases { + if bytes.Equal(mac, s.leases[i].HWAddr) { + return s.leases[i] + } + } return nil } +// Get next free IP +func (s *V4Server) findFreeIP() net.IP { + for i := s.conf.ipStart[3]; i != s.conf.ipEnd[3]; i++ { + if s.ipAddrs[i] == 0 { + ip := make([]byte, 4) + copy(ip, s.conf.ipStart) + ip[3] = i + return ip + } + } + return nil +} + +// Find an expired lease and return its index or -1 +func (s *V4Server) findExpiredLease() int { + now := time.Now().Unix() + for i, lease := range s.leases { + if lease.Expiry.Unix() != leaseExpireStatic && + lease.Expiry.Unix() <= now { + return i + } + } + return -1 +} + +// Reserve lease for MAC func (s *V4Server) reserveLease(mac net.HardwareAddr) *Lease { - return nil -} + l := Lease{} + l.HWAddr = make([]byte, 6) + copy(l.HWAddr, mac) -func (s *V4Server) commitLease(req *dhcpv4.DHCPv4, lease *Lease) time.Duration { - return 0 -} + l.IP = s.findFreeIP() + if l.IP == nil { + i := s.findExpiredLease() + if i < 0 { + return nil + } + copy(s.leases[i].HWAddr, mac) + return s.leases[i] + } -func (s *V4Server) checkIA(req *dhcpv4.DHCPv4, lease *Lease) error { - // req.GetOneOption(dhcpv4.OptionServerIdentifier) - return nil + s.addLease(&l) + return &l } // Find a lease associated with MAC and prepare response -func (s *V4Server) process(req *dhcpv4.DHCPv4, resp *dhcpv4.DHCPv4) bool { +func (s *V4Server) process(req *dhcpv4.DHCPv4, resp *dhcpv4.DHCPv4) int { + + var lease *Lease + mac := req.ClientHWAddr + if len(mac) != 6 { + log.Debug("DHCPv4: Invalid ClientHWAddr") + return -1 + } + hostname := req.Options.Get(dhcpv4.OptionHostName) + reqIP := req.Options.Get(dhcpv4.OptionRequestedIPAddress) - // lock + switch req.MessageType() { - lease := s.findLease(mac) - if lease == nil { - log.Debug("DHCPv6: no lease for: %s", mac) + case dhcpv4.MessageTypeDiscover: - switch req.MessageType() { + s.leasesLock.Lock() + defer s.leasesLock.Unlock() - case dhcpv4.MessageTypeDiscover: - lease = s.reserveLease(mac) - if lease == nil { - return false + lease = s.findLease(mac) + if lease == nil { + toStore := false + for lease == nil { + lease = s.reserveLease(mac) + if lease == nil { + log.Debug("DHCPv4: No more IP addresses") + if toStore { + s.conf.notify(LeaseChangedDBStore) + } + return 0 + } + + toStore = true + + if !s.addrAvailable(lease.IP) { + s.blacklistLease(lease) + lease = nil + continue + } + break } - default: - return false + s.conf.notify(LeaseChangedDBStore) + + // s.conf.notify(LeaseChangedBlacklisted) + + } else { + if len(reqIP) != 0 && + !bytes.Equal(reqIP, lease.IP) { + log.Debug("DHCPv4: different RequestedIP: %v != %v", reqIP, lease.IP) + } } + + lease.Hostname = string(hostname) + + resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer)) + + case dhcpv4.MessageTypeRequest: + + sid := req.Options.Get(dhcpv4.OptionServerIdentifier) + if len(sid) == 0 { + log.Debug("DHCPv4: No OptionServerIdentifier in Request message for %s", mac) + return -1 + } + if !bytes.Equal(sid, s.conf.dnsIPAddrs[0]) { + log.Debug("DHCPv4: Bad OptionServerIdentifier in Request message for %s", mac) + return -1 + } + + if len(reqIP) != 4 { + log.Debug("DHCPv4: Bad OptionRequestedIPAddress in Request message for %s", mac) + return -1 + } + + s.leasesLock.Lock() + for _, l := range s.leases { + if bytes.Equal(l.HWAddr, mac) { + if !bytes.Equal(l.IP, reqIP) { + s.leasesLock.Unlock() + log.Debug("DHCPv4: Mismatched OptionRequestedIPAddress in Request message for %s", mac) + return -1 + } + + if !bytes.Equal([]byte(l.Hostname), hostname) { + s.leasesLock.Unlock() + log.Debug("DHCPv4: Mismatched OptionHostName in Request message for %s", mac) + return -1 + } + + lease = l + break + } + } + s.leasesLock.Unlock() + + if lease == nil { + log.Debug("DHCPv4: No lease for %s", mac) + return 0 + } + + if lease.Expiry.Unix() != leaseExpireStatic { + + lease.Expiry = time.Now().Add(s.conf.leaseTime) + + s.leasesLock.Lock() + s.conf.notify(LeaseChangedDBStore) + s.leasesLock.Unlock() + + s.conf.notify(LeaseChangedAdded) + } + + resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) } - err := s.checkIA(req, lease) - if err != nil { - log.Debug("DHCPv4: %s", err) + resp.YourIPAddr = make([]byte, 4) + copy(resp.YourIPAddr, lease.IP) - // return NAK - - return false - } - - lifetime := s.commitLease(req, lease) - resp.UpdateOption(dhcpv4.OptIPAddressLeaseTime(lifetime)) - - // resp.UpdateOption(dhcpv4.OptRequestedIPAddress(x)) - resp.UpdateOption(dhcpv4.OptServerIdentifier(x)) - resp.UpdateOption(dhcpv4.OptDNS(s.conf.dnsIPAddrs...)) + resp.UpdateOption(dhcpv4.OptIPAddressLeaseTime(s.conf.leaseTime)) resp.UpdateOption(dhcpv4.OptRouter(s.conf.routerIP)) resp.UpdateOption(dhcpv4.OptSubnetMask(s.conf.subnetMask)) - return true + resp.UpdateOption(dhcpv4.OptDNS(s.conf.dnsIPAddrs...)) + return 1 } -// client -> Discover -> server -// client <- Reply <- server -// client -> Request -> server -// client <- Reply <- server +// client(0.0.0.0:68) -> (Request:ClientMAC,Discover,ClientID,ReqIP,HostName) -> server(255.255.255.255:67) +// client(255.255.255.255:68) <- (Reply:YourIP,ClientMAC,Offer,ServerID,SubnetMask,LeaseTime) <- server(:67) +// client(0.0.0.0:68) -> (Request:ClientMAC,Request,ClientID,ReqIP,HostName,ServerID,ParamReqList) -> server(255.255.255.255:67) +// client(255.255.255.255:68) <- (Reply:YourIP,ClientMAC,ACK,ServerID,SubnetMask,LeaseTime) <- server(:67) func (s *V4Server) packetHandler(conn net.PacketConn, peer net.Addr, req *dhcpv4.DHCPv4) { log.Debug("DHCPv4: received message: %s", req.Summary()) @@ -377,8 +472,14 @@ func (s *V4Server) packetHandler(conn net.PacketConn, peer net.Addr, req *dhcpv4 log.Debug("DHCPv4: dhcpv4.New: %s", err) return } + resp.UpdateOption(dhcpv4.OptServerIdentifier(s.conf.dnsIPAddrs[0])) - _ = s.process(req, resp) + r := s.process(req, resp) + if r < 0 { + return + } else if r == 0 { + resp.Options.Update(dhcpv4.OptMessageType(dhcpv4.MessageTypeNak)) + } log.Debug("DHCPv4: sending: %s", resp.Summary()) @@ -403,7 +504,7 @@ func getIfaceIPv4(iface net.Interface) []net.IP { continue } if ipnet.IP.To4() != nil { - res = append(res, ipnet.IP) + res = append(res, ipnet.IP.To4()) } } return res @@ -411,9 +512,13 @@ func getIfaceIPv4(iface net.Interface) []net.IP { // Start - start server func (s *V4Server) Start() error { + if !s.conf.Enabled { + return nil + } + iface, err := net.InterfaceByName(s.conf.InterfaceName) if err != nil { - return wrapErrPrint(err, "DHCPv4: Couldn't find interface by name %s", s.conf.InterfaceName) + return fmt.Errorf("DHCPv4: Couldn't find interface by name %s: %s", s.conf.InterfaceName, err) } log.Debug("DHCPv4: starting...") @@ -426,7 +531,6 @@ func (s *V4Server) Start() error { IP: net.ParseIP("0.0.0.0"), Port: dhcpv4.ServerPort, } - server, err := server4.NewServer(iface.Name, laddr, s.packetHandler, server4.WithDebugLogger()) if err != nil { return err @@ -446,7 +550,6 @@ func (s *V4Server) Start() error { func (s *V4Server) Reset() { s.leasesLock.Lock() s.leases = nil - s.IPpool = make(map[[4]byte]net.HardwareAddr) s.leasesLock.Unlock() } @@ -472,13 +575,34 @@ func v4Create(conf V4ServerConf) (*V4Server, error) { return s, nil } - s.conf.routerIP = x - s.conf.subnetMask = x + var err error + s.conf.routerIP, err = parseIPv4(s.conf.GatewayIP) + if err != nil { + return nil, fmt.Errorf("DHCPv4: %s", err) + } - // s.conf.ipStart = net.ParseIP(conf.RangeStart) - // if s.conf.ipStart == nil { - // return nil, fmt.Errorf("DHCPv6: invalid range-start IP: %s", conf.RangeStart) - // } + subnet, err := parseIPv4(s.conf.SubnetMask) + if err != nil || !isValidSubnetMask(subnet) { + return nil, fmt.Errorf("DHCPv4: invalid subnet mask: %s", s.conf.SubnetMask) + } + s.conf.subnetMask = make([]byte, 4) + copy(s.conf.subnetMask, subnet) + + s.conf.ipStart, err = parseIPv4(conf.RangeStart) + if s.conf.ipStart == nil { + return nil, fmt.Errorf("DHCPv4: %s", err) + } + if s.conf.ipStart[0] == 0 { + return nil, fmt.Errorf("DHCPv4: invalid range start IP") + } + s.conf.ipEnd, err = parseIPv4(conf.RangeEnd) + if s.conf.ipEnd == nil { + return nil, fmt.Errorf("DHCPv4: %s", err) + } + if !bytes.Equal(s.conf.ipStart[:3], s.conf.ipEnd[:3]) || + s.conf.ipStart[3] > s.conf.ipEnd[3] { + return nil, fmt.Errorf("DHCPv4: range end IP should match range start IP") + } // s.conf.ICMPTimeout = 1000 diff --git a/dhcpd/v6.go b/dhcpd/v6.go index 74af2129..e61ac9aa 100644 --- a/dhcpd/v6.go +++ b/dhcpd/v6.go @@ -467,6 +467,10 @@ func getIfaceIPv6(iface net.Interface) []net.IP { // Start - start server func (s *V6Server) Start() error { + if !s.conf.Enabled { + return nil + } + iface, err := net.InterfaceByName(s.conf.InterfaceName) if err != nil { return wrapErrPrint(err, "Couldn't find interface by name %s", s.conf.InterfaceName) diff --git a/home/config.go b/home/config.go index 35940ebd..a8b5f31e 100644 --- a/home/config.go +++ b/home/config.go @@ -126,10 +126,6 @@ var config = configuration{ PortHTTPS: 443, PortDNSOverTLS: 853, // needs to be passed through to dnsproxy }, - DHCP: dhcpd.ServerConfig{ - LeaseDuration: 86400, - ICMPTimeout: 1000, - }, SchemaVersion: currentSchemaVersion, } diff --git a/home/dhcp.go b/home/dhcp.go deleted file mode 100644 index 799333b0..00000000 --- a/home/dhcp.go +++ /dev/null @@ -1,36 +0,0 @@ -package home - -import ( - "github.com/joomcode/errorx" -) - -func startDHCPServer() error { - if !config.DHCP.Enabled { - // not enabled, don't do anything - return nil - } - - err := Context.dhcpServer.Init(config.DHCP) - if err != nil { - return errorx.Decorate(err, "Couldn't init DHCP server") - } - - err = Context.dhcpServer.Start() - if err != nil { - return errorx.Decorate(err, "Couldn't start DHCP server") - } - return nil -} - -func stopDHCPServer() error { - if !config.DHCP.Enabled { - return nil - } - - err := Context.dhcpServer.Stop() - if err != nil { - return errorx.Decorate(err, "Couldn't stop DHCP server") - } - - return nil -} diff --git a/home/home.go b/home/home.go index 2340ca6c..496e95c2 100644 --- a/home/home.go +++ b/home/home.go @@ -296,7 +296,7 @@ func run(args options) { } }() - err = startDHCPServer() + err = Context.dhcpServer.Start() if err != nil { log.Fatal(err) } @@ -447,7 +447,7 @@ func cleanup() { if err != nil { log.Error("Couldn't stop DNS server: %s", err) } - err = stopDHCPServer() + err = Context.dhcpServer.Stop() if err != nil { log.Error("Couldn't stop DHCP server: %s", err) }