mirror of
https://git.vectorsigma.ru/public/AdGuardHome.git
synced 2026-08-03 23:19:33 +00:00
Merge in DNS/adguard-home from 4923-gopacket-dhcp-vol.10 to master
Squashed commit of the following:
commit ef2106a35aadecf8340314535ec232f91ff44b50
Merge: f14e68781 f69da062e
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Mon Jun 9 16:13:52 2025 +0300
Merge branch 'master' into 4923-gopacket-dhcp-vol.10
commit f14e687815eb66a7c161dfaa3655c41fded643a8
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Fri Jun 6 18:36:32 2025 +0300
dhcpsvc: imp code, docs
commit c94b428d6fd277a840476f02820bd065cde17d6c
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Thu Jun 5 13:21:37 2025 +0300
dhcpsvc: imp code
commit 57eaea08e572fea41295d373b547f614a0c099ac
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Wed Jun 4 19:48:03 2025 +0300
dhcpsvc: add methods
commit b32218848cf2a90ecfc9cc6cf12c2579bcb1a2df
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Wed Apr 30 15:57:53 2025 +0300
dhcpsvc: add handlers
commit 615f1248160351f3bcdb040fb867d32630979d40
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Tue Jan 14 16:10:01 2025 +0300
dhcpsvc: multiplex dhcpv4
478 lines
15 KiB
Go
478 lines
15 KiB
Go
package dhcpsvc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"net/netip"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/AdguardTeam/golibs/errors"
|
|
"github.com/AdguardTeam/golibs/netutil"
|
|
"github.com/AdguardTeam/golibs/validate"
|
|
"github.com/google/gopacket/layers"
|
|
)
|
|
|
|
// 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 for calculating the
|
|
// network-specific broadcast address. It should be a valid IPv4 address,
|
|
// should be within the subnet, and should be outside the address range.
|
|
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.
|
|
// It should be a valid IPv4 address, should be within the subnet, and
|
|
// should be less or equal to RangeEnd.
|
|
RangeStart netip.Addr
|
|
|
|
// RangeEnd is the last address in the range to assign to DHCP clients. It
|
|
// should be a valid IPv4 address, should be within the subnet, and should
|
|
// be greater or equal to RangeStart.
|
|
RangeEnd netip.Addr
|
|
|
|
// Options is the list of explicitly configured DHCP options to send to
|
|
// 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. It should be positive.
|
|
LeaseDuration time.Duration
|
|
|
|
// Enabled is the state of the DHCPv4 service, whether it is enabled or not
|
|
// on the specific interface.
|
|
Enabled bool
|
|
}
|
|
|
|
// type check
|
|
var _ validate.Interface = (*IPv4Config)(nil)
|
|
|
|
// Validate implements the [validate.Interface] interface for *IPv4Config.
|
|
//
|
|
// TODO(e.burkov): Use [validate].
|
|
func (c *IPv4Config) Validate() (err error) {
|
|
if c == nil {
|
|
return errors.ErrNoValue
|
|
} else if !c.Enabled {
|
|
// Don't validate the configuration for disabled interface.
|
|
return nil
|
|
}
|
|
|
|
var errs []error
|
|
|
|
errs = c.validateSubnet(errs)
|
|
|
|
if c.LeaseDuration <= 0 {
|
|
err = newMustErr("icmp timeout", "be positive", c.LeaseDuration)
|
|
errs = append(errs, err)
|
|
}
|
|
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
// validateSubnet validates the subnet configuration.
|
|
func (c *IPv4Config) validateSubnet(errs []error) (res []error) {
|
|
res = errs
|
|
|
|
if !c.GatewayIP.Is4() {
|
|
err := newMustErr("gateway ip", "be a valid ipv4", c.GatewayIP)
|
|
res = append(res, err)
|
|
}
|
|
|
|
if !c.SubnetMask.Is4() {
|
|
err := newMustErr("subnet mask", "be a valid ipv4 cidr mask", c.SubnetMask)
|
|
res = append(res, err)
|
|
}
|
|
|
|
if !c.RangeStart.Is4() {
|
|
err := newMustErr("range start", "be a valid ipv4", c.RangeStart)
|
|
res = append(res, err)
|
|
}
|
|
|
|
if !c.RangeEnd.Is4() {
|
|
err := newMustErr("range end", "be a valid ipv4", c.RangeEnd)
|
|
res = append(res, err)
|
|
}
|
|
|
|
maskLen, _ := net.IPMask(c.SubnetMask.AsSlice()).Size()
|
|
subnet := netip.PrefixFrom(c.GatewayIP, maskLen)
|
|
|
|
switch {
|
|
case !subnet.Contains(c.RangeStart):
|
|
res = append(res, fmt.Errorf("range start %s is not within %s", c.RangeStart, subnet))
|
|
case !subnet.Contains(c.RangeEnd):
|
|
res = append(res, fmt.Errorf("range end %s is not within %s", c.RangeEnd, subnet))
|
|
}
|
|
|
|
addrSpace, err := newIPRange(c.RangeStart, c.RangeEnd)
|
|
if err != nil {
|
|
res = append(res, err)
|
|
} else if addrSpace.contains(c.GatewayIP) {
|
|
res = append(res, fmt.Errorf("gateway ip %s in the ip range %s", c.GatewayIP, addrSpace))
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
// dhcpInterfaceV4 is a DHCP interface for IPv4 address family.
|
|
type dhcpInterfaceV4 struct {
|
|
// common is the common part of any network interface within the DHCP
|
|
// server.
|
|
common *netInterface
|
|
|
|
// gateway is the IP address of the network gateway.
|
|
gateway netip.Addr
|
|
|
|
// subnet is the network subnet of the interface.
|
|
subnet netip.Prefix
|
|
|
|
// addrSpace is the IPv4 address space allocated for leasing.
|
|
addrSpace ipRange
|
|
|
|
// 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
|
|
}
|
|
|
|
// newDHCPInterfaceV4 creates a new DHCP interface for IPv4 address family with
|
|
// the given configuration. If the interface is disabled, it returns nil. conf
|
|
// must be valid.
|
|
func newDHCPInterfaceV4(
|
|
ctx context.Context,
|
|
l *slog.Logger,
|
|
name string,
|
|
conf *IPv4Config,
|
|
) (iface *dhcpInterfaceV4) {
|
|
if !conf.Enabled {
|
|
l.DebugContext(ctx, "disabled")
|
|
|
|
return nil
|
|
}
|
|
|
|
// TODO(e.burkov): Add a helper for converting [netip.Addr] to subnet mask
|
|
// to [netutil].
|
|
maskLen, _ := net.IPMask(conf.SubnetMask.AsSlice()).Size()
|
|
addrSpace, _ := newIPRange(conf.RangeStart, conf.RangeEnd)
|
|
|
|
iface = &dhcpInterfaceV4{
|
|
gateway: conf.GatewayIP,
|
|
subnet: netip.PrefixFrom(conf.GatewayIP, maskLen),
|
|
addrSpace: addrSpace,
|
|
common: &netInterface{
|
|
logger: l,
|
|
leases: map[macKey]*Lease{},
|
|
name: name,
|
|
leaseTTL: conf.LeaseDuration,
|
|
},
|
|
}
|
|
iface.implicitOpts, iface.explicitOpts = conf.options(ctx, l)
|
|
|
|
return iface
|
|
}
|
|
|
|
// 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 (c *IPv4Config) options(ctx context.Context, l *slog.Logger) (imp, exp layers.DHCPOptions) {
|
|
// Set default values of host configuration parameters listed in Appendix A
|
|
// of RFC-2131.
|
|
imp = layers.DHCPOptions{
|
|
// Values From Configuration
|
|
|
|
layers.NewDHCPOption(layers.DHCPOptSubnetMask, c.SubnetMask.AsSlice()),
|
|
layers.NewDHCPOption(layers.DHCPOptRouter, c.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(imp, compareV4OptionCodes)
|
|
|
|
// Set values for explicitly configured options.
|
|
for _, o := range c.Options {
|
|
i, found := slices.BinarySearchFunc(imp, o, compareV4OptionCodes)
|
|
if found {
|
|
imp = slices.Delete(imp, i, i+1)
|
|
}
|
|
|
|
i, found = slices.BinarySearchFunc(exp, o, compareV4OptionCodes)
|
|
if o.Length > 0 {
|
|
exp = slices.Insert(exp, i, o)
|
|
} else if found {
|
|
exp = slices.Delete(exp, i, i+1)
|
|
}
|
|
}
|
|
|
|
l.DebugContext(ctx, "options", "implicit", imp, "explicit", exp)
|
|
|
|
return imp, exp
|
|
}
|
|
|
|
// compareV4OptionCodes compares option codes of a and b.
|
|
func compareV4OptionCodes(a, b layers.DHCPOption) (res int) {
|
|
return int(a.Type) - int(b.Type)
|
|
}
|
|
|
|
// msg4Type returns the message type of msg, if it's present within the options.
|
|
func msg4Type(msg *layers.DHCPv4) (typ layers.DHCPMsgType, ok bool) {
|
|
for _, opt := range msg.Options {
|
|
if opt.Type == layers.DHCPOptMessageType && len(opt.Data) > 0 {
|
|
return layers.DHCPMsgType(opt.Data[0]), true
|
|
}
|
|
}
|
|
|
|
return 0, false
|
|
}
|
|
|
|
// requestedIPv4 returns the IPv4 address, requested by client in the DHCP
|
|
// message, if any.
|
|
//
|
|
// TODO(e.burkov): DRY with other IP-from-option helpers.
|
|
func requestedIPv4(msg *layers.DHCPv4) (ip netip.Addr, ok bool) {
|
|
for _, opt := range msg.Options {
|
|
if opt.Type == layers.DHCPOptRequestIP && len(opt.Data) == net.IPv4len {
|
|
return netip.AddrFromSlice(opt.Data)
|
|
}
|
|
}
|
|
|
|
return netip.Addr{}, false
|
|
}
|
|
|
|
// serverID4 returns the server ID of the DHCP message, if any.
|
|
func serverID4(msg *layers.DHCPv4) (ip netip.Addr, ok bool) {
|
|
for _, opt := range msg.Options {
|
|
if opt.Type == layers.DHCPOptServerID && len(opt.Data) == net.IPv4len {
|
|
return netip.AddrFromSlice(opt.Data)
|
|
}
|
|
}
|
|
|
|
return netip.Addr{}, false
|
|
}
|
|
|
|
// handleDiscover handles messages of type discover.
|
|
func (iface *dhcpInterfaceV4) handleDiscover(
|
|
ctx context.Context,
|
|
rw responseWriter4,
|
|
msg *layers.DHCPv4,
|
|
) {
|
|
// TODO(e.burkov): Implement.
|
|
}
|
|
|
|
// handleSelecting handles messages of type request in SELECTING state.
|
|
func (iface *dhcpInterfaceV4) handleSelecting(
|
|
ctx context.Context,
|
|
rw responseWriter4,
|
|
msg *layers.DHCPv4,
|
|
reqIP netip.Addr,
|
|
) {
|
|
// TODO(e.burkov): Implement.
|
|
}
|
|
|
|
// handleSelecting handles messages of type request in INIT-REBOOT state.
|
|
func (iface *dhcpInterfaceV4) handleInitReboot(
|
|
ctx context.Context,
|
|
rw responseWriter4,
|
|
msg *layers.DHCPv4,
|
|
reqIP netip.Addr,
|
|
) {
|
|
// TODO(e.burkov): Implement.
|
|
}
|
|
|
|
// handleRenew handles messages of type request in RENEWING or REBINDING state.
|
|
func (iface *dhcpInterfaceV4) handleRenew(
|
|
ctx context.Context,
|
|
rw responseWriter4,
|
|
req *layers.DHCPv4,
|
|
) {
|
|
// TODO(e.burkov): Implement.
|
|
}
|
|
|
|
// dhcpInterfacesV4 is a slice of network interfaces of IPv4 address family.
|
|
type dhcpInterfacesV4 []*dhcpInterfaceV4
|
|
|
|
// find returns the first network interface within ifaces containing ip. It
|
|
// returns false if there is no such interface.
|
|
func (ifaces dhcpInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok bool) {
|
|
i := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV4) (contains bool) {
|
|
return iface.subnet.Contains(ip)
|
|
})
|
|
if i < 0 {
|
|
return nil, false
|
|
}
|
|
|
|
return ifaces[i].common, true
|
|
}
|
|
|
|
// findInterface returns the first DHCPv4 interface within ifaces containing
|
|
// ip. It returns false if there is no such interface.
|
|
func (ifaces dhcpInterfacesV4) findInterface(ip netip.Addr) (iface *dhcpInterfaceV4, ok bool) {
|
|
i := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV4) (contains bool) {
|
|
return iface.subnet.Contains(ip)
|
|
})
|
|
if i < 0 {
|
|
return nil, false
|
|
}
|
|
|
|
return ifaces[i], true
|
|
}
|