AdGuardHome/internal/dhcpsvc/v4.go

321 lines
11 KiB
Go

package dhcpsvc
import (
"fmt"
"net"
"net/netip"
"time"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/google/gopacket/layers"
"golang.org/x/exp/slices"
)
// IPv4Config is the interface-specific configuration for DHCPv4.
type IPv4Config struct {
// GatewayIP is the IPv4 address of the network's gateway. It is used as
// the default gateway for DHCP clients and also used in calculating the
// network-specific broadcast address.
GatewayIP netip.Addr
// SubnetMask is the IPv4 subnet mask of the network. It should be a valid
// IPv4 CIDR (i.e. all 1s followed by all 0s).
SubnetMask netip.Addr
// RangeStart is the first address in the range to assign to DHCP clients.
RangeStart netip.Addr
// RangeEnd is the last address in the range to assign to DHCP clients.
RangeEnd netip.Addr
// Options is the list of DHCP options to send to DHCP clients. The options
// having a zero value within the Length field are treated as deletions of
// the corresponding options, either implicit or explicit.
Options layers.DHCPOptions
// LeaseDuration is the TTL of a DHCP lease.
LeaseDuration time.Duration
// Enabled is the state of the DHCPv4 service, whether it is enabled or not
// on the specific interface.
Enabled bool
}
// validate returns an error in conf if any.
func (conf *IPv4Config) validate() (err error) {
switch {
case conf == nil:
return errNilConfig
case !conf.Enabled:
return nil
case !conf.GatewayIP.Is4():
return newMustErr("gateway ip", "be a valid ipv4", conf.GatewayIP)
case !conf.SubnetMask.Is4():
return newMustErr("subnet mask", "be a valid ipv4 cidr mask", conf.SubnetMask)
case !conf.RangeStart.Is4():
return newMustErr("range start", "be a valid ipv4", conf.RangeStart)
case !conf.RangeEnd.Is4():
return newMustErr("range end", "be a valid ipv4", conf.RangeEnd)
case conf.LeaseDuration <= 0:
return newMustErr("lease duration", "be less than %d", conf.LeaseDuration)
default:
return nil
}
}
// iface4 is a DHCP interface for IPv4 address family.
type iface4 struct {
// gateway is the IP address of the network gateway.
gateway netip.Addr
// subnet is the network subnet.
subnet netip.Prefix
// addrSpace is the IPv4 address space allocated for leasing.
addrSpace ipRange
// name is the name of the interface.
name string
// implicitOpts are the options listed in Appendix A of RFC 2131 and
// initialized with default values. It must not have intersections with
// explicitOpts.
implicitOpts layers.DHCPOptions
// explicitOpts are the user-configured options. It must not have
// intersections with implicitOpts.
explicitOpts layers.DHCPOptions
// leaseTTL is the time-to-live of dynamic leases on this interface.
leaseTTL time.Duration
}
// newIface4 creates a new DHCP interface for IPv4 address family with the given
// configuration. It returns an error if the given configuration can't be used.
func newIface4(name string, conf *IPv4Config) (i *iface4, err error) {
if !conf.Enabled {
return nil, nil
}
maskLen, _ := net.IPMask(conf.SubnetMask.AsSlice()).Size()
subnet := netip.PrefixFrom(conf.GatewayIP, maskLen)
switch {
case !subnet.Contains(conf.RangeStart):
return nil, fmt.Errorf("range start %s is not within %s", conf.RangeStart, subnet)
case !subnet.Contains(conf.RangeEnd):
return nil, fmt.Errorf("range end %s is not within %s", conf.RangeEnd, subnet)
}
addrSpace, err := newIPRange(conf.RangeStart, conf.RangeEnd)
if err != nil {
return nil, err
} else if addrSpace.contains(conf.GatewayIP) {
return nil, fmt.Errorf("gateway ip %s in the ip range %s", conf.GatewayIP, addrSpace)
}
i = &iface4{
name: name,
gateway: conf.GatewayIP,
subnet: subnet,
addrSpace: addrSpace,
leaseTTL: conf.LeaseDuration,
}
i.implicitOpts, i.explicitOpts = conf.options()
return i, nil
}
// options returns the implicit and explicit options for the interface. The two
// lists are disjoint and the implicit options are initialized with default
// values.
//
// TODO(e.burkov): DRY with the IPv6 version.
func (conf *IPv4Config) options() (implicit, explicit layers.DHCPOptions) {
// Set default values of host configuration parameters listed in Appendix A
// of RFC-2131.
implicit = layers.DHCPOptions{
// Values From Configuration
layers.NewDHCPOption(layers.DHCPOptSubnetMask, conf.SubnetMask.AsSlice()),
layers.NewDHCPOption(layers.DHCPOptRouter, conf.GatewayIP.AsSlice()),
// IP-Layer Per Host
// An Internet host that includes embedded gateway code MUST have a
// configuration switch to disable the gateway function, and this switch
// MUST default to the non-gateway mode.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.5.
layers.NewDHCPOption(layers.DHCPOptIPForwarding, []byte{0x0}),
// A host that supports non-local source-routing MUST have a
// configurable switch to disable forwarding, and this switch MUST
// default to disabled.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.5.
layers.NewDHCPOption(layers.DHCPOptSourceRouting, []byte{0x0}),
// Do not set the Policy Filter Option since it only makes sense when
// the non-local source routing is enabled.
// The minimum legal value is 576.
//
// See https://datatracker.ietf.org/doc/html/rfc2132#section-4.4.
layers.NewDHCPOption(layers.DHCPOptDatagramMTU, []byte{0x2, 0x40}),
// Set the current recommended default time to live for the Internet
// Protocol which is 64.
//
// See https://www.iana.org/assignments/ip-parameters/ip-parameters.xhtml#ip-parameters-2.
layers.NewDHCPOption(layers.DHCPOptDefaultTTL, []byte{0x40}),
// For example, after the PTMU estimate is decreased, the timeout should
// be set to 10 minutes; once this timer expires and a larger MTU is
// attempted, the timeout can be set to a much smaller value.
//
// See https://datatracker.ietf.org/doc/html/rfc1191#section-6.6.
layers.NewDHCPOption(layers.DHCPOptPathMTUAgingTimeout, []byte{0x0, 0x0, 0x2, 0x58}),
// There is a table describing the MTU values representing all major
// data-link technologies in use in the Internet so that each set of
// similar MTUs is associated with a plateau value equal to the lowest
// MTU in the group.
//
// See https://datatracker.ietf.org/doc/html/rfc1191#section-7.
layers.NewDHCPOption(layers.DHCPOptPathPlateuTableOption, []byte{
0x0, 0x44,
0x1, 0x28,
0x1, 0xFC,
0x3, 0xEE,
0x5, 0xD4,
0x7, 0xD2,
0x11, 0x0,
0x1F, 0xE6,
0x45, 0xFA,
}),
// IP-Layer Per Interface
// Don't set the Interface MTU because client may choose the value on
// their own since it's listed in the [Host Requirements RFC]. It also
// seems the values listed there sometimes appear obsolete, see
// https://github.com/AdguardTeam/AdGuardHome/issues/5281.
//
// [Host Requirements RFC]: https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.3.
// Set the All Subnets Are Local Option to false since commonly the
// connected hosts aren't expected to be multihomed.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.3.
layers.NewDHCPOption(layers.DHCPOptAllSubsLocal, []byte{0x0}),
// Set the Perform Mask Discovery Option to false to provide the subnet
// mask by options only.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.2.9.
layers.NewDHCPOption(layers.DHCPOptMaskDiscovery, []byte{0x0}),
// A system MUST NOT send an Address Mask Reply unless it is an
// authoritative agent for address masks. An authoritative agent may be
// a host or a gateway, but it MUST be explicitly configured as a
// address mask agent.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.2.9.
layers.NewDHCPOption(layers.DHCPOptMaskSupplier, []byte{0x0}),
// Set the Perform Router Discovery Option to true as per Router
// Discovery Document.
//
// See https://datatracker.ietf.org/doc/html/rfc1256#section-5.1.
layers.NewDHCPOption(layers.DHCPOptRouterDiscovery, []byte{0x1}),
// The all-routers address is preferred wherever possible.
//
// See https://datatracker.ietf.org/doc/html/rfc1256#section-5.1.
layers.NewDHCPOption(layers.DHCPOptSolicitAddr, netutil.IPv4allrouter()),
// Don't set the Static Routes Option since it should be set up by
// system administrator.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.1.2.
// A datagram with the destination address of limited broadcast will be
// received by every host on the connected physical network but will not
// be forwarded outside that network.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.1.3.
layers.NewDHCPOption(layers.DHCPOptBroadcastAddr, netutil.IPv4bcast()),
// Link-Layer Per Interface
// If the system does not dynamically negotiate use of the trailer
// protocol on a per-destination basis, the default configuration MUST
// disable the protocol.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-2.3.1.
layers.NewDHCPOption(layers.DHCPOptARPTrailers, []byte{0x0}),
// For proxy ARP situations, the timeout needs to be on the order of a
// minute.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-2.3.2.1.
layers.NewDHCPOption(layers.DHCPOptARPTimeout, []byte{0x0, 0x0, 0x0, 0x3C}),
// An Internet host that implements sending both the RFC-894 and the
// RFC-1042 encapsulations MUST provide a configuration switch to select
// which is sent, and this switch MUST default to RFC-894.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-2.3.3.
layers.NewDHCPOption(layers.DHCPOptEthernetEncap, []byte{0x0}),
// TCP Per Host
// A fixed value must be at least big enough for the Internet diameter,
// i.e., the longest possible path. A reasonable value is about twice
// the diameter, to allow for continued Internet growth.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.1.7.
layers.NewDHCPOption(layers.DHCPOptTCPTTL, []byte{0x0, 0x0, 0x0, 0x3C}),
// The interval MUST be configurable and MUST default to no less than
// two hours.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-4.2.3.6.
layers.NewDHCPOption(layers.DHCPOptTCPKeepAliveInt, []byte{0x0, 0x0, 0x1C, 0x20}),
// Unfortunately, some misbehaved TCP implementations fail to respond to
// a probe segment unless it contains data.
//
// See https://datatracker.ietf.org/doc/html/rfc1122#section-4.2.3.6.
layers.NewDHCPOption(layers.DHCPOptTCPKeepAliveGarbage, []byte{0x1}),
}
slices.SortFunc(implicit, compareV4OptionCodes)
// Set values for explicitly configured options.
for _, exp := range conf.Options {
i, found := slices.BinarySearchFunc(implicit, exp, compareV4OptionCodes)
if found {
implicit = slices.Delete(implicit, i, i+1)
}
i, found = slices.BinarySearchFunc(explicit, exp, compareV4OptionCodes)
if exp.Length > 0 {
explicit = slices.Insert(explicit, i, exp)
} else if found {
explicit = slices.Delete(explicit, i, i+1)
}
}
log.Debug("dhcpsvc: v4: implicit options: %s", implicit)
log.Debug("dhcpsvc: v4: explicit options: %s", explicit)
return implicit, explicit
}
// compareV4OptionCodes compares option codes of a and b.
func compareV4OptionCodes(a, b layers.DHCPOption) (res int) {
return int(a.Type) - int(b.Type)
}