package dhcpd import ( "fmt" "net" "net/netip" "time" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/golibs/errors" ) // ServerConfig is the configuration for the DHCP server. The order of YAML // fields is important, since the YAML configuration file follows it. type ServerConfig struct { // Called when the configuration is changed by HTTP request ConfigModified func() `yaml:"-"` // Register an HTTP handler HTTPRegister aghhttp.RegisterFunc `yaml:"-"` Enabled bool `yaml:"enabled"` InterfaceName string `yaml:"interface_name"` // LocalDomainName is the domain name used for DHCP hosts. For example, a // DHCP client with the hostname "myhost" can be addressed as "myhost.lan" // when LocalDomainName is "lan". // // TODO(e.burkov): Probably, remove this field. See the TODO on // [Interface.Enabled]. LocalDomainName string `yaml:"local_domain_name"` Conf4 V4ServerConf `yaml:"dhcpv4"` Conf6 V6ServerConf `yaml:"dhcpv6"` // WorkDir is used to store DHCP leases. // // Deprecated: Remove it when migration of DHCP leases will not be needed. WorkDir string `yaml:"-"` // DataDir is used to store DHCP leases. DataDir string `yaml:"-"` // dbFilePath is the path to the file with stored DHCP leases. dbFilePath string `yaml:"-"` } // DHCPServer - DHCP server interface type DHCPServer interface { // ResetLeases resets leases. ResetLeases(leases []*Lease) (err error) // GetLeases returns deep clones of the current leases. GetLeases(flags GetLeasesFlags) (leases []*Lease) // AddStaticLease - add a static lease AddStaticLease(l *Lease) (err error) // RemoveStaticLease - remove a static lease RemoveStaticLease(l *Lease) (err error) // FindMACbyIP returns a MAC address by the IP address of its lease, if // there is one. FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) // HostByIP returns a hostname by the IP address of its lease, if there is // one. HostByIP(ip netip.Addr) (host string) // IPByHost returns an IP address by the hostname of its lease, if there is // one. IPByHost(host string) (ip netip.Addr) // WriteDiskConfig4 - copy disk configuration WriteDiskConfig4(c *V4ServerConf) // WriteDiskConfig6 - copy disk configuration WriteDiskConfig6(c *V6ServerConf) // Start - start server Start() (err error) // Stop - stop server Stop() (err error) getLeasesRef() []*Lease } // V4ServerConf - server configuration type V4ServerConf struct { Enabled bool `yaml:"-" json:"-"` InterfaceName string `yaml:"-" json:"-"` GatewayIP netip.Addr `yaml:"gateway_ip" json:"gateway_ip"` SubnetMask netip.Addr `yaml:"subnet_mask" json:"subnet_mask"` // broadcastIP is the broadcasting address pre-calculated from the // configured gateway IP and subnet mask. broadcastIP netip.Addr // The first & the last IP address for dynamic leases // Bytes [0..2] of the last allowed IP address must match the first IP RangeStart netip.Addr `yaml:"range_start" json:"range_start"` RangeEnd netip.Addr `yaml:"range_end" json:"range_end"` LeaseDuration uint32 `yaml:"lease_duration" json:"lease_duration"` // in seconds // IP conflict detector: time (ms) to wait for ICMP reply // 0: disable ICMPTimeout uint32 `yaml:"icmp_timeout_msec" json:"-"` // Custom Options. // // Option with arbitrary hexadecimal data: // DEC_CODE hex HEX_DATA // where DEC_CODE is a decimal DHCPv4 option code in range [1..255] // // Option with IP data (only 1 IP is supported): // DEC_CODE ip IP_ADDR Options []string `yaml:"options" json:"-"` ipRange *ipRange leaseTime time.Duration // the time during which a dynamic lease is considered valid dnsIPAddrs []netip.Addr // IPv4 addresses to return to DHCP clients as DNS server addresses // subnet contains the DHCP server's subnet. The IP is the IP of the // gateway. subnet netip.Prefix // notify is a way to signal to other components that leases have been // changed. notify must be called outside of locked sections, since the // clients might want to get the new data. // // TODO(a.garipov): This is utter madness and must be refactored. It just // begs for deadlock bugs and other nastiness. notify func(uint32) } // errNilConfig is an error returned by validation method if the config is nil. const errNilConfig errors.Error = "nil config" // ensureV4 returns an unmapped version of ip. An error is returned if the // passed ip is not an IPv4. func ensureV4(ip netip.Addr, kind string) (ip4 netip.Addr, err error) { ip4 = ip.Unmap() if !ip4.IsValid() || !ip4.Is4() { return netip.Addr{}, fmt.Errorf("%v is not an IPv4 %s", ip, kind) } return ip4, nil } // Validate returns an error if c is not a valid configuration. // // TODO(e.burkov): Don't set the config fields when the server itself will stop // containing the config. func (c *V4ServerConf) Validate() (err error) { defer func() { err = errors.Annotate(err, "dhcpv4: %w") }() if c == nil { return errNilConfig } gatewayIP, err := ensureV4(c.GatewayIP, "address") if err != nil { // Don't wrap the error since it's informative enough as is and there is // an annotation deferred already. return err } subnetMask, err := ensureV4(c.SubnetMask, "subnet mask") if err != nil { // Don't wrap the error since it's informative enough as is and there is // an annotation deferred already. return err } maskLen, _ := net.IPMask(subnetMask.AsSlice()).Size() c.subnet = netip.PrefixFrom(gatewayIP, maskLen) c.broadcastIP = aghnet.BroadcastFromPref(c.subnet) rangeStart, err := ensureV4(c.RangeStart, "address") if err != nil { // Don't wrap the error since it's informative enough as is and there is // an annotation deferred already. return err } rangeEnd, err := ensureV4(c.RangeEnd, "address") if err != nil { // Don't wrap the error since it's informative enough as is and there is // an annotation deferred already. return err } c.ipRange, err = newIPRange(rangeStart.AsSlice(), rangeEnd.AsSlice()) if err != nil { // Don't wrap the error since it's informative enough as is and there is // an annotation deferred already. return err } if c.ipRange.contains(gatewayIP.AsSlice()) { return fmt.Errorf("gateway ip %v in the ip range: %v-%v", gatewayIP, c.RangeStart, c.RangeEnd, ) } if !c.subnet.Contains(rangeStart) { return fmt.Errorf("range start %v is outside network %v", c.RangeStart, c.subnet, ) } if !c.subnet.Contains(rangeEnd) { return fmt.Errorf("range end %v is outside network %v", c.RangeEnd, c.subnet, ) } return nil } // V6ServerConf - server configuration type V6ServerConf struct { Enabled bool `yaml:"-" json:"-"` InterfaceName string `yaml:"-" json:"-"` // The first IP address for dynamic leases // The last allowed IP address ends with 0xff byte RangeStart net.IP `yaml:"range_start" json:"range_start"` LeaseDuration uint32 `yaml:"lease_duration" json:"lease_duration"` // in seconds RASLAACOnly bool `yaml:"ra_slaac_only" json:"-"` // send ICMPv6.RA packets without MO flags RAAllowSLAAC bool `yaml:"ra_allow_slaac" json:"-"` // send ICMPv6.RA packets with MO flags ipStart net.IP // starting IP address for dynamic leases leaseTime time.Duration // the time during which a dynamic lease is considered valid dnsIPAddrs []net.IP // IPv6 addresses to return to DHCP clients as DNS server addresses // Server calls this function when leases data changes notify func(uint32) }