AdGuardHome/internal/dhcpd/v4.go
Eugene Burkov da86620288 Pull request: 3443 dhcp broadcast vol.2
Merge in DNS/adguard-home from 3443-dhcp-broadcast-vol.2 to master

Closes #3443.

Squashed commit of the following:

commit a85af89cb43f2489126fe3c12366fc034e89f59d
Merge: 72eb3a88 a4e07827
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Sep 30 18:08:19 2021 +0300

    Merge branch 'master' into 3443-dhcp-broadcast-vol.2

commit 72eb3a8853540b06ee1096decf50e836b539fe45
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Sep 30 18:03:19 2021 +0300

    dhcpd: imp code readability

commit 2d1fbc40d04a4125855d6be9f02e09d15430150d
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Sep 30 14:16:59 2021 +0300

    dhcpd: imp tests

commit 889fad3084ad2b81edfc12100e2ce29d323227ba
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Sep 29 20:09:25 2021 +0300

    dhcpd: imp code, docs

commit 1fd6b2346ff66e033bceaa169aed751be5822ca8
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Sep 23 16:08:18 2021 +0300

    dhcpd: unicast to mac address
2021-09-30 18:28:19 +03:00

1139 lines
26 KiB
Go

//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
package dhcpd
import (
"bytes"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/go-ping/ping"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/server4"
"github.com/mdlayher/raw"
)
// v4Server is a DHCPv4 server.
//
// TODO(a.garipov): Think about unifying this and v6Server.
type v4Server struct {
conf V4ServerConf
srv *server4.Server
// leasedOffsets contains offsets from conf.ipRange.start that have been
// leased.
leasedOffsets *bitSet
// leaseHosts is the set of all hostnames of all known DHCP clients.
leaseHosts *stringutil.Set
// leases contains all dynamic and static leases.
leases []*Lease
// leasesLock protects leases, leaseHosts, and leasedOffsets.
leasesLock sync.Mutex
// options holds predefined DHCP options to return to clients.
options dhcpv4.Options
}
// WriteDiskConfig4 - write configuration
func (s *v4Server) WriteDiskConfig4(c *V4ServerConf) {
*c = s.conf
}
// WriteDiskConfig6 - write configuration
func (s *v4Server) WriteDiskConfig6(c *V6ServerConf) {
}
// normalizeHostname normalizes a hostname sent by the client. If err is not
// nil, norm is an empty string.
func normalizeHostname(hostname string) (norm string, err error) {
defer func() { err = errors.Annotate(err, "normalizing %q: %w", hostname) }()
if hostname == "" {
return "", nil
}
norm = strings.ToLower(hostname)
parts := strings.FieldsFunc(norm, func(c rune) (ok bool) {
return c != '.' && !netutil.IsValidHostOuterRune(c)
})
if len(parts) == 0 {
return "", fmt.Errorf("no valid parts")
}
norm = strings.Join(parts, "-")
norm = strings.TrimSuffix(norm, "-")
return norm, nil
}
// validHostnameForClient accepts the hostname sent by the client and its IP and
// returns either a normalized version of that hostname, or a new hostname
// generated from the IP address, or an empty string.
func (s *v4Server) validHostnameForClient(cliHostname string, ip net.IP) (hostname string) {
hostname, err := normalizeHostname(cliHostname)
if err != nil {
log.Info("dhcpv4: %s", err)
}
if hostname == "" {
hostname = aghnet.GenerateHostname(ip)
}
err = netutil.ValidateDomainName(hostname)
if err != nil {
log.Info("dhcpv4: %s", err)
hostname = ""
}
return hostname
}
// ResetLeases resets leases.
func (s *v4Server) ResetLeases(leases []*Lease) (err error) {
defer func() { err = errors.Annotate(err, "dhcpv4: %w") }()
if !s.conf.Enabled {
return
}
s.leasedOffsets = newBitSet()
s.leaseHosts = stringutil.NewSet()
s.leases = nil
for _, l := range leases {
if !l.IsStatic() {
l.Hostname = s.validHostnameForClient(l.Hostname, l.IP)
}
err = s.addLease(l)
if err != nil {
// TODO(a.garipov): Wrap and bubble up the error.
log.Error(
"dhcpv4: reset: re-adding a lease for %s (%s): %s",
l.IP,
l.HWAddr,
err,
)
continue
}
}
return nil
}
// getLeasesRef returns the actual leases slice. For internal use only.
func (s *v4Server) getLeasesRef() []*Lease {
return s.leases
}
// isBlocklisted returns true if this lease holds a blocklisted IP.
//
// TODO(a.garipov): Make a method of *Lease?
func (s *v4Server) isBlocklisted(l *Lease) (ok bool) {
if len(l.HWAddr) == 0 {
return false
}
ok = true
for _, b := range l.HWAddr {
if b != 0 {
ok = false
break
}
}
return ok
}
// GetLeases returns the list of current DHCP leases. It is safe for concurrent
// use.
func (s *v4Server) GetLeases(flags GetLeasesFlags) (leases []*Lease) {
// The function shouldn't return nil, because zero-length slice behaves
// differently in cases like marshalling. Our front-end also requires
// a non-nil value in the response.
leases = []*Lease{}
getDynamic := flags&LeasesDynamic != 0
getStatic := flags&LeasesStatic != 0
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
now := time.Now()
for _, l := range s.leases {
if getDynamic && l.Expiry.After(now) && !s.isBlocklisted(l) {
leases = append(leases, l.Clone())
continue
}
if getStatic && l.IsStatic() {
leases = append(leases, l.Clone())
}
}
return leases
}
// FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases
func (s *v4Server) FindMACbyIP(ip net.IP) net.HardwareAddr {
now := time.Now()
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
ip4 := ip.To4()
if ip4 == nil {
return nil
}
for _, l := range s.leases {
if l.IP.Equal(ip4) {
if l.Expiry.After(now) || l.IsStatic() {
return l.HWAddr
}
}
}
return nil
}
// defaultHwAddrLen is the default length of a hardware (MAC) address.
const defaultHwAddrLen = 6
// Add the specified IP to the black list for a time period
func (s *v4Server) blocklistLease(l *Lease) {
l.HWAddr = make(net.HardwareAddr, defaultHwAddrLen)
l.Hostname = ""
l.Expiry = time.Now().Add(s.conf.leaseTime)
}
// rmLeaseByIndex removes a lease by its index in the leases slice.
func (s *v4Server) rmLeaseByIndex(i int) {
n := len(s.leases)
if i >= n {
// TODO(a.garipov): Better error handling.
log.Debug("dhcpv4: can't remove lease at index %d: no such lease", i)
return
}
l := s.leases[i]
s.leases = append(s.leases[:i], s.leases[i+1:]...)
r := s.conf.ipRange
offset, ok := r.offset(l.IP)
if ok {
s.leasedOffsets.set(offset, false)
}
s.leaseHosts.Del(l.Hostname)
log.Debug("dhcpv4: removed lease %s (%s)", l.IP, l.HWAddr)
}
// Remove a dynamic lease with the same properties
// Return error if a static lease is found
func (s *v4Server) rmDynamicLease(lease *Lease) (err error) {
for i := 0; i < len(s.leases); i++ {
l := s.leases[i]
if bytes.Equal(l.HWAddr, lease.HWAddr) {
if l.IsStatic() {
return errors.Error("static lease already exists")
}
s.rmLeaseByIndex(i)
if i == len(s.leases) {
break
}
l = s.leases[i]
}
if l.IP.Equal(lease.IP) {
if l.IsStatic() {
return errors.Error("static lease already exists")
}
s.rmLeaseByIndex(i)
if i == len(s.leases) {
break
}
l = s.leases[i]
}
if l.Hostname == lease.Hostname {
l.Hostname = ""
}
}
return nil
}
// addLease adds a dynamic or static lease.
func (s *v4Server) addLease(l *Lease) (err error) {
r := s.conf.ipRange
offset, inOffset := r.offset(l.IP)
if l.IsStatic() {
if sn := s.conf.subnet; !sn.Contains(l.IP) {
return fmt.Errorf("subnet %s does not contain the ip %q", sn, l.IP)
}
} else if !inOffset {
return fmt.Errorf("lease %s (%s) out of range, not adding", l.IP, l.HWAddr)
}
s.leases = append(s.leases, l)
s.leasedOffsets.set(offset, true)
if l.Hostname != "" {
s.leaseHosts.Add(l.Hostname)
}
return nil
}
// rmLease removes a lease with the same properties.
func (s *v4Server) rmLease(lease *Lease) (err error) {
if len(s.leases) == 0 {
return nil
}
for i, l := range s.leases {
if l.IP.Equal(lease.IP) {
if !bytes.Equal(l.HWAddr, lease.HWAddr) || l.Hostname != lease.Hostname {
return fmt.Errorf("lease for ip %s is different: %+v", lease.IP, l)
}
s.rmLeaseByIndex(i)
return nil
}
}
return errors.Error("lease not found")
}
// AddStaticLease adds a static lease. It is safe for concurrent use.
func (s *v4Server) AddStaticLease(l *Lease) (err error) {
defer func() { err = errors.Annotate(err, "dhcpv4: adding static lease: %w") }()
if ip4 := l.IP.To4(); ip4 == nil {
return fmt.Errorf("invalid ip %q, only ipv4 is supported", l.IP)
}
l.Expiry = time.Unix(leaseExpireStatic, 0)
err = netutil.ValidateMAC(l.HWAddr)
if err != nil {
return err
}
if hostname := l.Hostname; hostname != "" {
hostname, err = normalizeHostname(hostname)
if err != nil {
return err
}
err = netutil.ValidateDomainName(hostname)
if err != nil {
return fmt.Errorf("validating hostname: %w", err)
}
// Don't check for hostname uniqueness, since we try to emulate
// dnsmasq here, which means that rmDynamicLease below will
// simply empty the hostname of the dynamic lease if there even
// is one.
l.Hostname = hostname
}
// Perform the following actions in an anonymous function to make sure
// that the lock gets unlocked before the notification step.
func() {
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
err = s.rmDynamicLease(l)
if err != nil {
err = fmt.Errorf(
"removing dynamic leases for %s (%s): %w",
l.IP,
l.HWAddr,
err,
)
return
}
err = s.addLease(l)
if err != nil {
err = fmt.Errorf("adding static lease for %s (%s): %w", l.IP, l.HWAddr, err)
return
}
}()
if err != nil {
return err
}
s.conf.notify(LeaseChangedDBStore)
s.conf.notify(LeaseChangedAddedStatic)
return nil
}
// RemoveStaticLease removes a static lease. It is safe for concurrent use.
func (s *v4Server) RemoveStaticLease(l *Lease) (err error) {
defer func() { err = errors.Annotate(err, "dhcpv4: %w") }()
if len(l.IP) != 4 {
return fmt.Errorf("invalid IP")
}
err = netutil.ValidateMAC(l.HWAddr)
if err != nil {
return fmt.Errorf("validating lease: %w", err)
}
s.leasesLock.Lock()
err = s.rmLease(l)
if err != nil {
s.leasesLock.Unlock()
return err
}
s.leasesLock.Unlock()
s.conf.notify(LeaseChangedDBStore)
s.conf.notify(LeaseChangedRemovedStatic)
return nil
}
// addrAvailable sends an ICP request to the specified IP address. It returns
// true if the remote host doesn't reply, which probably means that the IP
// address is available.
//
// TODO(a.garipov): I'm not sure that this is the best way to do this.
func (s *v4Server) addrAvailable(target net.IP) (avail bool) {
if s.conf.ICMPTimeout == 0 {
return true
}
pinger, err := ping.NewPinger(target.String())
if err != nil {
log.Error("dhcpv4: ping.NewPinger(): %s", err)
return true
}
pinger.SetPrivileged(true)
pinger.Timeout = time.Duration(s.conf.ICMPTimeout) * time.Millisecond
pinger.Count = 1
reply := false
pinger.OnRecv = func(_ *ping.Packet) {
reply = true
}
log.Debug("dhcpv4: sending icmp echo to %s", target)
err = pinger.Run()
if err != nil {
log.Error("dhcpv4: pinger.Run(): %s", err)
return true
}
if reply {
log.Info("dhcpv4: ip conflict: %s is already used by another device", target)
return false
}
log.Debug("dhcpv4: icmp procedure is complete: %q", target)
return true
}
// findLease finds a lease by its MAC-address.
func (s *v4Server) findLease(mac net.HardwareAddr) (l *Lease) {
for _, l = range s.leases {
if bytes.Equal(mac, l.HWAddr) {
return l
}
}
return nil
}
// nextIP generates a new free IP.
func (s *v4Server) nextIP() (ip net.IP) {
r := s.conf.ipRange
ip = r.find(func(next net.IP) (ok bool) {
offset, ok := r.offset(next)
if !ok {
// Shouldn't happen.
return false
}
return !s.leasedOffsets.isSet(offset)
})
return ip.To4()
}
// Find an expired lease and return its index or -1
func (s *v4Server) findExpiredLease() int {
now := time.Now()
for i, lease := range s.leases {
if !lease.IsStatic() && lease.Expiry.Before(now) {
return i
}
}
return -1
}
// reserveLease reserves a lease for a client by its MAC-address. It returns
// nil if it couldn't allocate a new lease.
func (s *v4Server) reserveLease(mac net.HardwareAddr) (l *Lease, err error) {
l = &Lease{
HWAddr: make([]byte, len(mac)),
}
copy(l.HWAddr, mac)
l.IP = s.nextIP()
if l.IP == nil {
i := s.findExpiredLease()
if i < 0 {
return nil, nil
}
copy(s.leases[i].HWAddr, mac)
return s.leases[i], nil
}
err = s.addLease(l)
if err != nil {
return nil, err
}
return l, nil
}
func (s *v4Server) commitLease(l *Lease) {
l.Expiry = time.Now().Add(s.conf.leaseTime)
func() {
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
s.conf.notify(LeaseChangedDBStore)
if l.Hostname != "" {
s.leaseHosts.Add(l.Hostname)
}
}()
s.conf.notify(LeaseChangedAdded)
}
// allocateLease allocates a new lease for the MAC address. If there are no IP
// addresses left, both l and err are nil.
func (s *v4Server) allocateLease(mac net.HardwareAddr) (l *Lease, err error) {
for {
l, err = s.reserveLease(mac)
if err != nil {
return nil, fmt.Errorf("reserving a lease: %w", err)
} else if l == nil {
return nil, nil
}
if s.addrAvailable(l.IP) {
return l, nil
}
s.blocklistLease(l)
}
}
// processDiscover is the handler for the DHCP Discover request.
func (s *v4Server) processDiscover(req, resp *dhcpv4.DHCPv4) (l *Lease, err error) {
mac := req.ClientHWAddr
defer s.conf.notify(LeaseChangedDBStore)
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
l = s.findLease(mac)
if l != nil {
reqIP := req.RequestedIPAddress()
if len(reqIP) != 0 && !reqIP.Equal(l.IP) {
log.Debug("dhcpv4: different RequestedIP: %s != %s", reqIP, l.IP)
}
resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))
return l, nil
}
l, err = s.allocateLease(mac)
if err != nil {
return nil, err
} else if l == nil {
log.Debug("dhcpv4: no more ip addresses")
return nil, nil
}
resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))
return l, nil
}
type optFQDN struct {
name string
}
func (o *optFQDN) String() string {
return "optFQDN"
}
// flags[1]
// A-RR[1]
// PTR-RR[1]
// name[]
func (o *optFQDN) ToBytes() []byte {
b := make([]byte, 3+len(o.name))
i := 0
b[i] = 0x03 // f_server_overrides | f_server
i++
b[i] = 255 // A-RR
i++
b[i] = 255 // PTR-RR
i++
copy(b[i:], []byte(o.name))
return b
}
// checkLease checks if the pair of mac and ip is already leased. The mismatch
// is true when the existing lease has the same hardware address but differs in
// its IP address.
func (s *v4Server) checkLease(mac net.HardwareAddr, ip net.IP) (lease *Lease, mismatch bool) {
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
for _, l := range s.leases {
if !bytes.Equal(l.HWAddr, mac) {
continue
}
if l.IP.Equal(ip) {
return l, false
}
log.Debug(
`dhcpv4: mismatched OptionRequestedIPAddress in req msg for %s`,
mac,
)
return nil, true
}
return nil, false
}
// processRequest is the handler for the DHCP Request request.
func (s *v4Server) processRequest(req, resp *dhcpv4.DHCPv4) (lease *Lease, needsReply bool) {
mac := req.ClientHWAddr
reqIP := req.RequestedIPAddress()
if reqIP == nil {
reqIP = req.ClientIPAddr
}
sid := req.ServerIdentifier()
if len(sid) != 0 && !sid.Equal(s.conf.dnsIPAddrs[0]) {
log.Debug("dhcpv4: bad OptionServerIdentifier in req msg for %s", mac)
return nil, false
}
if ip4 := reqIP.To4(); ip4 == nil {
log.Debug("dhcpv4: bad OptionRequestedIPAddress in req msg for %s", mac)
return nil, false
}
var mismatch bool
if lease, mismatch = s.checkLease(mac, reqIP); mismatch {
return nil, true
}
if lease == nil {
log.Debug("dhcpv4: no reserved lease for %s", mac)
return nil, true
}
if !lease.IsStatic() {
cliHostname := req.HostName()
hostname := s.validHostnameForClient(cliHostname, reqIP)
if hostname != lease.Hostname && s.leaseHosts.Has(hostname) {
log.Info("dhcpv4: hostname %q already exists", hostname)
lease.Hostname = ""
} else {
lease.Hostname = hostname
}
s.commitLease(lease)
} else if lease.Hostname != "" {
o := &optFQDN{
name: lease.Hostname,
}
fqdn := dhcpv4.Option{
Code: dhcpv4.OptionFQDN,
Value: o,
}
resp.UpdateOption(fqdn)
}
resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
return lease, true
}
// processRequest is the handler for the DHCP Decline request.
func (s *v4Server) processDecline(req, resp *dhcpv4.DHCPv4) (err error) {
s.conf.notify(LeaseChangedDBStore)
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
mac := req.ClientHWAddr
reqIP := req.RequestedIPAddress()
if reqIP == nil {
reqIP = req.ClientIPAddr
}
var oldLease *Lease
for _, l := range s.leases {
if bytes.Equal(l.HWAddr, mac) && l.IP.Equal(reqIP) {
oldLease = l
break
}
}
if oldLease == nil {
log.Info("dhcpv4: lease with ip %s for %s not found", reqIP, mac)
return nil
}
err = s.rmDynamicLease(oldLease)
if err != nil {
return fmt.Errorf("removing old lease for %s: %w", mac, err)
}
newLease, err := s.allocateLease(mac)
if err != nil {
return fmt.Errorf("allocating new lease for %s: %w", mac, err)
} else if newLease == nil {
log.Info("dhcpv4: allocating new lease for %s: no more ip addresses", mac)
resp.YourIPAddr = make([]byte, 4)
resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
return nil
}
newLease.Hostname = oldLease.Hostname
newLease.Expiry = time.Now().Add(s.conf.leaseTime)
err = s.addLease(newLease)
if err != nil {
return fmt.Errorf("adding new lease for %s: %w", mac, err)
}
log.Info("dhcpv4: changed ip from %s to %s for %s", reqIP, newLease.IP, mac)
resp.YourIPAddr = make([]byte, 4)
copy(resp.YourIPAddr, newLease.IP)
resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
return nil
}
// processRelease is the handler for the DHCP Release request.
func (s *v4Server) processRelease(req, resp *dhcpv4.DHCPv4) (err error) {
mac := req.ClientHWAddr
reqIP := req.RequestedIPAddress()
if reqIP == nil {
reqIP = req.ClientIPAddr
}
// TODO(a.garipov): Add a separate notification type for dynamic lease
// removal?
defer s.conf.notify(LeaseChangedDBStore)
n := 0
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
for _, l := range s.leases {
if !bytes.Equal(l.HWAddr, mac) || !l.IP.Equal(reqIP) {
continue
}
err = s.rmDynamicLease(l)
if err != nil {
err = fmt.Errorf("removing dynamic lease for %s: %w", mac, err)
return
}
n++
}
log.Info("dhcpv4: released %d dynamic leases for %s", n, mac)
resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
return nil
}
// Find a lease associated with MAC and prepare response
// Return 1: OK
// Return 0: error; reply with Nak
// Return -1: error; don't reply
func (s *v4Server) process(req, resp *dhcpv4.DHCPv4) int {
var err error
// Include server's identifier option since any reply should contain it.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#page-29.
resp.UpdateOption(dhcpv4.OptServerIdentifier(s.conf.dnsIPAddrs[0]))
// TODO(a.garipov): Refactor this into handlers.
var l *Lease
switch req.MessageType() {
case dhcpv4.MessageTypeDiscover:
l, err = s.processDiscover(req, resp)
if err != nil {
log.Error("dhcpv4: processing discover: %s", err)
return 0
}
if l == nil {
return 0
}
case dhcpv4.MessageTypeRequest:
var toReply bool
l, toReply = s.processRequest(req, resp)
if l == nil {
if toReply {
return 0
}
return -1 // drop packet
}
case dhcpv4.MessageTypeDecline:
err = s.processDecline(req, resp)
if err != nil {
log.Error("dhcpv4: processing decline: %s", err)
return 0
}
case dhcpv4.MessageTypeRelease:
err = s.processRelease(req, resp)
if err != nil {
log.Error("dhcpv4: processing release: %s", err)
return 0
}
}
if l != nil {
resp.YourIPAddr = netutil.CloneIP(l.IP)
}
// Set IP address lease time for all DHCPOFFER messages and DHCPACK
// messages replied for DHCPREQUEST.
//
// TODO(e.burkov): Inspect why this is always set to configured value.
resp.UpdateOption(dhcpv4.OptIPAddressLeaseTime(s.conf.leaseTime))
// Update values for each explicitly configured parameter requested by
// client.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.1.
requested := req.ParameterRequestList()
for _, code := range requested {
if configured := s.options; configured.Has(code) {
resp.UpdateOption(dhcpv4.OptGeneric(code, configured.Get(code)))
}
}
// Update the value of Domain Name Server option separately from others
// since its value is set after server's creating.
if requested.Has(dhcpv4.OptionDomainNameServer) {
resp.UpdateOption(dhcpv4.OptDNS(s.conf.dnsIPAddrs...))
}
return 1
}
// client(0.0.0.0:68) -> (Request:ClientMAC,Type=Discover,ClientID,ReqIP,HostName) -> server(255.255.255.255:67)
// client(255.255.255.255:68) <- (Reply:YourIP,ClientMAC,Type=Offer,ServerID,SubnetMask,LeaseTime) <- server(<IP>:67)
// client(0.0.0.0:68) -> (Request:ClientMAC,Type=Request,ClientID,ReqIP||ClientIP,HostName,ServerID,ParamReqList) -> server(255.255.255.255:67)
// client(255.255.255.255:68) <- (Reply:YourIP,ClientMAC,Type=ACK,ServerID,SubnetMask,LeaseTime) <- server(<IP>:67)
func (s *v4Server) packetHandler(conn net.PacketConn, peer net.Addr, req *dhcpv4.DHCPv4) {
log.Debug("dhcpv4: received message: %s", req.Summary())
switch req.MessageType() {
case
dhcpv4.MessageTypeDiscover,
dhcpv4.MessageTypeRequest,
dhcpv4.MessageTypeDecline,
dhcpv4.MessageTypeRelease:
// Go on.
default:
log.Debug("dhcpv4: unsupported message type %d", req.MessageType())
return
}
resp, err := dhcpv4.NewReplyFromRequest(req)
if err != nil {
log.Debug("dhcpv4: dhcpv4.New: %s", err)
return
}
err = netutil.ValidateMAC(req.ClientHWAddr)
if err != nil {
log.Error("dhcpv4: invalid ClientHWAddr: %s", err)
return
}
r := s.process(req, resp)
if r < 0 {
return
} else if r == 0 {
resp.Options.Update(dhcpv4.OptMessageType(dhcpv4.MessageTypeNak))
}
s.send(peer, conn, req, resp)
}
// send writes resp for peer to conn considering the req's parameters according
// to RFC-2131.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.1.
func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DHCPv4) {
switch giaddr, ciaddr, mtype := req.GatewayIPAddr, req.ClientIPAddr, resp.MessageType(); {
case giaddr != nil && !giaddr.IsUnspecified():
// Send any return messages to the server port on the BOOTP
// relay agent whose address appears in giaddr.
peer = &net.UDPAddr{
IP: giaddr,
Port: dhcpv4.ServerPort,
}
if mtype == dhcpv4.MessageTypeNak {
// Set the broadcast bit in the DHCPNAK, so that the
// relay agent broadcasted it to the client, because the
// client may not have a correct network address or
// subnet mask, and the client may not be answering ARP
// requests.
resp.SetBroadcast()
}
case mtype == dhcpv4.MessageTypeNak:
// Broadcast any DHCPNAK messages to 0xffffffff.
case ciaddr != nil && !ciaddr.IsUnspecified():
// Unicast DHCPOFFER and DHCPACK messages to the address in
// ciaddr.
peer = &net.UDPAddr{
IP: ciaddr,
Port: dhcpv4.ClientPort,
}
case !req.IsBroadcast() && req.ClientHWAddr != nil:
// Unicast DHCPOFFER and DHCPACK messages to the client's
// hardware address and yiaddr.
peer = &dhcpUnicastAddr{
Addr: raw.Addr{HardwareAddr: req.ClientHWAddr},
yiaddr: resp.YourIPAddr,
}
default:
// Go on since peer is already set to broadcast.
}
log.Debug("dhcpv4: sending to %s: %s", peer, resp.Summary())
if _, err := conn.WriteTo(resp.ToBytes(), peer); err != nil {
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err)
}
}
// Start starts the IPv4 DHCP server.
func (s *v4Server) Start() (err error) {
defer func() { err = errors.Annotate(err, "dhcpv4: %w") }()
if !s.conf.Enabled {
return nil
}
ifaceName := s.conf.InterfaceName
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
return fmt.Errorf("finding interface %s by name: %w", ifaceName, err)
}
log.Debug("dhcpv4: starting...")
dnsIPAddrs, err := aghnet.IfaceDNSIPAddrs(
iface,
aghnet.IPVersion4,
defaultMaxAttempts,
defaultBackoff,
)
if err != nil {
return fmt.Errorf("interface %s: %w", ifaceName, err)
}
if len(dnsIPAddrs) == 0 {
// No available IP addresses which may appear later.
return nil
}
s.conf.dnsIPAddrs = dnsIPAddrs
var c net.PacketConn
if c, err = s.newDHCPConn(iface); err != nil {
return err
}
s.srv, err = server4.NewServer(
iface.Name,
nil,
s.packetHandler,
server4.WithConn(c),
server4.WithDebugLogger(),
)
if err != nil {
return err
}
log.Info("dhcpv4: listening")
go func() {
if serr := s.srv.Serve(); errors.Is(serr, net.ErrClosed) {
log.Info("dhcpv4: server is closed")
return
} else if serr != nil {
log.Error("dhcpv4: srv.Serve: %s", serr)
}
}()
// Signal to the clients containers in packages home and dnsforward that
// it should reload the DHCP clients.
s.conf.notify(LeaseChangedAdded)
return nil
}
// Stop - stop server
func (s *v4Server) Stop() (err error) {
if s.srv == nil {
return
}
log.Debug("dhcpv4: stopping")
err = s.srv.Close()
if err != nil {
return fmt.Errorf("closing dhcpv4 srv: %w", err)
}
// Signal to the clients containers in packages home and dnsforward that
// it should remove all DHCP clients.
s.conf.notify(LeaseChangedRemovedAll)
s.srv = nil
return nil
}
// Create DHCPv4 server
func v4Create(conf V4ServerConf) (srv DHCPServer, err error) {
s := &v4Server{}
s.conf = conf
s.leaseHosts = stringutil.NewSet()
// TODO(a.garipov): Don't use a disabled server in other places or just
// use an interface.
if !conf.Enabled {
return s, nil
}
var routerIP net.IP
routerIP, err = tryTo4(s.conf.GatewayIP)
if err != nil {
return s, fmt.Errorf("dhcpv4: %w", err)
}
if s.conf.SubnetMask == nil {
return s, fmt.Errorf("dhcpv4: invalid subnet mask: %v", s.conf.SubnetMask)
}
subnetMask := make([]byte, 4)
copy(subnetMask, s.conf.SubnetMask.To4())
s.conf.subnet = &net.IPNet{
IP: routerIP,
Mask: subnetMask,
}
s.conf.broadcastIP = aghnet.BroadcastFromIPNet(s.conf.subnet)
s.conf.ipRange, err = newIPRange(conf.RangeStart, conf.RangeEnd)
if err != nil {
return s, fmt.Errorf("dhcpv4: %w", err)
}
s.leasedOffsets = newBitSet()
if conf.LeaseDuration == 0 {
s.conf.leaseTime = time.Hour * 24
s.conf.LeaseDuration = uint32(s.conf.leaseTime.Seconds())
} else {
s.conf.leaseTime = time.Second * time.Duration(conf.LeaseDuration)
}
s.options = prepareOptions(s.conf)
return s, nil
}