diff --git a/internal/dhcpsvc/interface.go b/internal/dhcpsvc/interface.go index 9686e463..f9f79679 100644 --- a/internal/dhcpsvc/interface.go +++ b/internal/dhcpsvc/interface.go @@ -34,7 +34,7 @@ func macToKey(mac net.HardwareAddr) (key macKey) { type netInterface struct { // logger logs the events related to the network interface. // - // TODO(e.burkov): Consider removing it and using the valud from context. + // TODO(e.burkov): Consider removing it and using the value from context. logger *slog.Logger // leases is the set of DHCP leases assigned to this interface. diff --git a/internal/dhcpsvc/options4.go b/internal/dhcpsvc/options4.go index 837ff32a..4171d3f1 100644 --- a/internal/dhcpsvc/options4.go +++ b/internal/dhcpsvc/options4.go @@ -13,15 +13,12 @@ import ( "github.com/google/gopacket/layers" ) -// 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) { +// implicitOptions returns the implicit options for the interface, sorted by +// code. +func (c *IPv4Config) implicitOptions() (opts layers.DHCPOptions) { // Set default values of host configuration parameters listed in Appendix A // of RFC-2131. - imp = layers.DHCPOptions{ + opts = layers.DHCPOptions{ // Values From Configuration layers.NewDHCPOption(layers.DHCPOptSubnetMask, c.SubnetMask.AsSlice()), @@ -177,7 +174,20 @@ func (c *IPv4Config) options(ctx context.Context, l *slog.Logger) (imp, exp laye // See https://datatracker.ietf.org/doc/html/rfc1122#section-4.2.3.6. layers.NewDHCPOption(layers.DHCPOptTCPKeepAliveGarbage, []byte{0x1}), } - slices.SortFunc(imp, compareV4OptionCodes) + slices.SortFunc(opts, compareV4OptionCodes) + + return opts +} + +// options returns the implicit and explicit options for the interface. The two +// lists are disjoint and the implicit options are initialized with default +// values. All options within exp which have a nil Data field should be treated +// as instruction to remove those from responses. +// +// TODO(e.burkov): DRY with the IPv6 version. +func (c *IPv4Config) options(ctx context.Context, l *slog.Logger) (imp, exp layers.DHCPOptions) { + // Set values of implicit options. + imp = c.implicitOptions() // Set values for explicitly configured options. for _, o := range c.Options { @@ -187,10 +197,10 @@ func (c *IPv4Config) options(ctx context.Context, l *slog.Logger) (imp, exp laye } i, found = slices.BinarySearchFunc(exp, o, compareV4OptionCodes) - if o.Length > 0 { + if found { + exp[i].Data, exp[i].Length = o.Data, o.Length + } else { exp = slices.Insert(exp, i, o) - } else if found { - exp = slices.Delete(exp, i, i+1) } } @@ -205,11 +215,44 @@ func compareV4OptionCodes(a, b layers.DHCPOption) (res int) { } // updateOptions updates the options of the response in accordance with the -// request and RFC 2131. +// requested parameters. req and resp must not be nil. // // See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.1. func (iface *dhcpInterfaceV4) updateOptions(req, resp *layers.DHCPv4) { - // TODO(e.burkov): !! Implement + // If the server recognizes the parameter as a parameter defined in the Host + // Requirements Document, the server MUST include the default value for that + // parameter. + optWithCode := layers.DHCPOption{} + for _, code := range requestedOptions(req) { + optWithCode.Type = code + i, has := slices.BinarySearchFunc(iface.implicitOpts, optWithCode, compareV4OptionCodes) + if has { + // The client MAY list the options in order of preference. The DHCP + // server is not required to return the options in the requested + // order, but MUST try to insert the requested options in the order + // requested by the client. + // + // See https://datatracker.ietf.org/doc/html/rfc2132#section-9.8. + resp.Options = append(resp.Options, iface.implicitOpts[i]) + } + } + + // If the server has been explicitly configured with a default value for the + // parameter or the parameter has a non-default value on the client's + // subnet, the server MUST include that value in an appropriate option. + for _, opt := range iface.explicitOpts { + if opt.Data != nil { + resp.Options = append(resp.Options, opt) + + continue + } + + // Remove options explicitly configured to be removed, in case they are + // already set. + resp.Options = slices.DeleteFunc(resp.Options, func(o layers.DHCPOption) (ok bool) { + return o.Type == opt.Type + }) + } } // appendLeaseTime appends the lease time option to the response. @@ -268,3 +311,23 @@ func hostname4(msg *layers.DHCPv4) (hostname string) { return "" } + +// requestedOptions returns the list of options requested in DHCPv4 message, if +// any. +func requestedOptions(msg *layers.DHCPv4) (opts []layers.DHCPOpt) { + for _, opt := range msg.Options { + l := len(opt.Data) + if opt.Type != layers.DHCPOptParamsRequest || l == 0 { + continue + } + + opts = make([]layers.DHCPOpt, 0, l) + for _, code := range opt.Data { + opts = append(opts, layers.DHCPOpt(code)) + } + + return opts + } + + return nil +} diff --git a/internal/dhcpsvc/v4_internal_test.go b/internal/dhcpsvc/options4_internal_test.go similarity index 95% rename from internal/dhcpsvc/v4_internal_test.go rename to internal/dhcpsvc/options4_internal_test.go index 7aa64505..44e75e26 100644 --- a/internal/dhcpsvc/v4_internal_test.go +++ b/internal/dhcpsvc/options4_internal_test.go @@ -49,7 +49,9 @@ func TestIPv4Config_Options(t *testing.T) { layers.NewDHCPOption(layers.DHCPOptBroadcastAddr, nil), }, }, - wantExplicit: nil, + wantExplicit: layers.DHCPOptions{ + layers.NewDHCPOption(layers.DHCPOptBroadcastAddr, nil), + }, }, { name: "rewritten_del", conf: &IPv4Config{ diff --git a/internal/dhcpsvc/v4.go b/internal/dhcpsvc/v4.go index 7cb1fd05..5c268269 100644 --- a/internal/dhcpsvc/v4.go +++ b/internal/dhcpsvc/v4.go @@ -41,9 +41,9 @@ type IPv4Config struct { 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. + // clients. Options with nil Data field are removed from responses. + // + // TODO(e.burkov): Validate. Options layers.DHCPOptions // LeaseDuration is the TTL of a DHCP lease. It should be positive. @@ -146,7 +146,8 @@ type dhcpInterfaceV4 struct { implicitOpts layers.DHCPOptions // explicitOpts are the user-configured options. It must not have - // intersections with implicitOpts. + // intersections with implicitOpts. Options with nil Data field are removed + // from responses. explicitOpts layers.DHCPOptions }