diff --git a/internal/dhcpsvc/v4.go b/internal/dhcpsvc/v4.go index 44c4f840..e74c06b3 100644 --- a/internal/dhcpsvc/v4.go +++ b/internal/dhcpsvc/v4.go @@ -6,7 +6,10 @@ import ( "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. @@ -26,7 +29,9 @@ type IPv4Config struct { // 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. + // 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. @@ -73,7 +78,14 @@ type iface4 struct { // name is the name of the interface. name string - // TODO(e.burkov): Add options. + // 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 @@ -103,11 +115,206 @@ func newIface4(name string, conf *IPv4Config) (i *iface4, err error) { return nil, fmt.Errorf("gateway ip %s in the ip range %s", conf.GatewayIP, addrSpace) } - return &iface4{ + i = &iface4{ name: name, gateway: conf.GatewayIP, subnet: subnet, addrSpace: addrSpace, leaseTTL: conf.LeaseDuration, - }, nil + } + 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) } diff --git a/internal/dhcpsvc/v4_internal_test.go b/internal/dhcpsvc/v4_internal_test.go new file mode 100644 index 00000000..0b65366e --- /dev/null +++ b/internal/dhcpsvc/v4_internal_test.go @@ -0,0 +1,88 @@ +package dhcpsvc + +import ( + "net/netip" + "testing" + + "github.com/google/gopacket/layers" + "github.com/stretchr/testify/assert" +) + +func TestIPv4Config_Options(t *testing.T) { + oneIP, otherIP := netip.MustParseAddr("1.2.3.4"), netip.MustParseAddr("5.6.7.8") + subnetMask := netip.MustParseAddr("255.255.0.0") + + opt1 := layers.NewDHCPOption(layers.DHCPOptSubnetMask, subnetMask.AsSlice()) + opt6 := layers.NewDHCPOption(layers.DHCPOptDNS, append(oneIP.AsSlice(), otherIP.AsSlice()...)) + opt28 := layers.NewDHCPOption(layers.DHCPOptBroadcastAddr, oneIP.AsSlice()) + opt121 := layers.NewDHCPOption(layers.DHCPOptClasslessStaticRoute, []byte("cba")) + + testCases := []struct { + name string + conf *IPv4Config + wantExplicit layers.DHCPOptions + }{{ + name: "all_default", + conf: &IPv4Config{ + Options: nil, + }, + wantExplicit: nil, + }, { + name: "configured_ip", + conf: &IPv4Config{ + Options: layers.DHCPOptions{opt28}, + }, + wantExplicit: layers.DHCPOptions{opt28}, + }, { + name: "configured_ips", + conf: &IPv4Config{ + Options: layers.DHCPOptions{opt6}, + }, + wantExplicit: layers.DHCPOptions{opt6}, + }, { + name: "configured_del", + conf: &IPv4Config{ + Options: layers.DHCPOptions{ + layers.NewDHCPOption(layers.DHCPOptBroadcastAddr, nil), + }, + }, + wantExplicit: nil, + }, { + name: "rewritten_del", + conf: &IPv4Config{ + Options: layers.DHCPOptions{ + layers.NewDHCPOption(layers.DHCPOptBroadcastAddr, nil), + opt28, + }, + }, + wantExplicit: layers.DHCPOptions{opt28}, + }, { + name: "configured_and_del", + conf: &IPv4Config{ + Options: layers.DHCPOptions{ + layers.NewDHCPOption(layers.DHCPOptClasslessStaticRoute, []byte("a")), + layers.NewDHCPOption(layers.DHCPOptClasslessStaticRoute, nil), + opt121, + }, + }, + wantExplicit: layers.DHCPOptions{opt121}, + }, { + name: "replace_config_value", + conf: &IPv4Config{ + SubnetMask: netip.MustParseAddr("255.255.255.0"), + Options: layers.DHCPOptions{opt1}, + }, + wantExplicit: layers.DHCPOptions{opt1}, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + imp, exp := tc.conf.options() + assert.Equal(t, tc.wantExplicit, exp) + + for c := range exp { + assert.NotContains(t, imp, c) + } + }) + } +} diff --git a/internal/dhcpsvc/v6.go b/internal/dhcpsvc/v6.go index 8bdc1637..2dc832b6 100644 --- a/internal/dhcpsvc/v6.go +++ b/internal/dhcpsvc/v6.go @@ -5,7 +5,9 @@ import ( "net/netip" "time" + "github.com/AdguardTeam/golibs/log" "github.com/google/gopacket/layers" + "golang.org/x/exp/slices" ) // IPv6Config is the interface-specific configuration for DHCPv6. @@ -13,8 +15,10 @@ type IPv6Config struct { // RangeStart is the first address in the range to assign to DHCP clients. RangeStart netip.Addr - // Options is the list of DHCP options to send to DHCP clients. - Options layers.DHCPOptions + // Options is the list of DHCP options to send to DHCP clients. The options + // with zero length are treated as deletions of the corresponding options, + // either implicit or explicit. + Options layers.DHCPv6Options // LeaseDuration is the TTL of a DHCP lease. LeaseDuration time.Duration @@ -58,6 +62,15 @@ type iface6 struct { // name is the name of the interface. name string + // implicitOpts are the DHCPv6 options listed in RFC 8415 (and others) and + // initialized with default values. It must not have intersections with + // explicitOpts. + implicitOpts layers.DHCPv6Options + + // explicitOpts are the user-configured options. It must not have + // intersections with implicitOpts. + explicitOpts layers.DHCPv6Options + // leaseTTL is the time-to-live of dynamic leases on this interface. leaseTTL time.Duration @@ -78,11 +91,45 @@ func newIface6(name string, conf *IPv6Config) (i *iface6) { return nil } - return &iface6{ + i = &iface6{ name: name, rangeStart: conf.RangeStart, leaseTTL: conf.LeaseDuration, raSLAACOnly: conf.RASLAACOnly, raAllowSLAAC: conf.RAAllowSLAAC, } + i.implicitOpts, i.explicitOpts = conf.options() + + return i +} + +// 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): Add implicit options according to RFC. +func (conf *IPv6Config) options() (implicit, explicit layers.DHCPv6Options) { + // Set default values of host configuration parameters listed in RFC 8415. + implicit = layers.DHCPv6Options{} + slices.SortFunc(implicit, compareV6OptionCodes) + + // Set values for explicitly configured options. + for _, exp := range conf.Options { + i, found := slices.BinarySearchFunc(implicit, exp, compareV6OptionCodes) + if found { + implicit = slices.Delete(implicit, i, i+1) + } + + explicit = append(explicit, exp) + } + + log.Debug("dhcpsvc: v6: implicit options: %s", implicit) + log.Debug("dhcpsvc: v6: explicit options: %s", explicit) + + return implicit, explicit +} + +// compareV6OptionCodes compares option codes of a and b. +func compareV6OptionCodes(a, b layers.DHCPv6Option) (res int) { + return int(a.Code) - int(b.Code) }