Pull request 2256: 4923 Better interfaces

Updates #4923.

Squashed commit of the following:

commit 0e40b41aa1e517a62d6076c4e7a57c607792ef01
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Jul 10 15:28:16 2024 +0300

    dhcpsvc: imp code, docs

commit 5463fdde473f84caaca229b53027e8183d5c6bdc
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Jul 9 20:31:20 2024 +0300

    dhcpsvc: imp ifaces
This commit is contained in:
Eugene Burkov 2024-07-10 16:17:56 +03:00
parent c0a33ce708
commit 42c7cd6f8e
8 changed files with 138 additions and 107 deletions

View File

@ -50,7 +50,7 @@ type Interface interface {
IPByHost(host string) (ip netip.Addr) IPByHost(host string) (ip netip.Addr)
// Leases returns all the active DHCP leases. The returned slice should be // Leases returns all the active DHCP leases. The returned slice should be
// a clone. // a clone. The order of leases is undefined.
// //
// TODO(e.burkov): Consider implementing iterating methods with appropriate // TODO(e.burkov): Consider implementing iterating methods with appropriate
// signatures instead of cloning the whole list. // signatures instead of cloning the whole list.

View File

@ -3,42 +3,74 @@ package dhcpsvc
import ( import (
"fmt" "fmt"
"log/slog" "log/slog"
"slices" "net"
"time" "time"
) )
// netInterface is a common part of any network interface within the DHCP // macKey contains hardware address as byte array of 6, 8, or 20 bytes.
// server. //
// TODO(e.burkov): Move to aghnet or even to netutil.
type macKey any
// macToKey converts mac into macKey, which is used as the key for the lease
// maps. mac must be a valid hardware address of length 6, 8, or 20 bytes, see
// [netutil.ValidateMAC].
func macToKey(mac net.HardwareAddr) (key macKey) {
switch len(mac) {
case 6:
return [6]byte(mac)
case 8:
return [8]byte(mac)
case 20:
return [20]byte(mac)
default:
panic(fmt.Errorf("invalid mac address %#v", mac))
}
}
// netInterface is a common part of any interface within the DHCP server.
// //
// TODO(e.burkov): Add other methods as [DHCPServer] evolves. // TODO(e.burkov): Add other methods as [DHCPServer] evolves.
type netInterface struct { type netInterface struct {
// logger logs the events related to the network interface. // logger logs the events related to the network interface.
logger *slog.Logger logger *slog.Logger
// leases is the set of DHCP leases assigned to this interface.
leases map[macKey]*Lease
// name is the name of the network interface. // name is the name of the network interface.
name string name string
// leases is a set of leases sorted by hardware address.
leases []*Lease
// leaseTTL is the default Time-To-Live value for leases. // leaseTTL is the default Time-To-Live value for leases.
leaseTTL time.Duration leaseTTL time.Duration
} }
// reset clears all the slices in iface for reuse. // newNetInterface creates a new netInterface with the given name, leaseTTL, and
func (iface *netInterface) reset() { // logger.
iface.leases = iface.leases[:0] func newNetInterface(name string, l *slog.Logger, leaseTTL time.Duration) (iface *netInterface) {
return &netInterface{
logger: l,
leases: map[macKey]*Lease{},
name: name,
leaseTTL: leaseTTL,
}
} }
// insertLease inserts the given lease into iface. It returns an error if the // reset clears all the slices in iface for reuse.
func (iface *netInterface) reset() {
clear(iface.leases)
}
// addLease inserts the given lease into iface. It returns an error if the
// lease can't be inserted. // lease can't be inserted.
func (iface *netInterface) insertLease(l *Lease) (err error) { func (iface *netInterface) addLease(l *Lease) (err error) {
i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC) mk := macToKey(l.HWAddr)
_, found := iface.leases[mk]
if found { if found {
return fmt.Errorf("lease for mac %s already exists", l.HWAddr) return fmt.Errorf("lease for mac %s already exists", l.HWAddr)
} }
iface.leases = slices.Insert(iface.leases, i, l) iface.leases[mk] = l
return nil return nil
} }
@ -46,12 +78,13 @@ func (iface *netInterface) insertLease(l *Lease) (err error) {
// updateLease replaces an existing lease within iface with the given one. It // updateLease replaces an existing lease within iface with the given one. It
// returns an error if there is no lease with such hardware address. // returns an error if there is no lease with such hardware address.
func (iface *netInterface) updateLease(l *Lease) (prev *Lease, err error) { func (iface *netInterface) updateLease(l *Lease) (prev *Lease, err error) {
i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC) mk := macToKey(l.HWAddr)
prev, found := iface.leases[mk]
if !found { if !found {
return nil, fmt.Errorf("no lease for mac %s", l.HWAddr) return nil, fmt.Errorf("no lease for mac %s", l.HWAddr)
} }
prev, iface.leases[i] = iface.leases[i], l iface.leases[mk] = l
return prev, nil return prev, nil
} }
@ -59,12 +92,13 @@ func (iface *netInterface) updateLease(l *Lease) (prev *Lease, err error) {
// removeLease removes an existing lease from iface. It returns an error if // removeLease removes an existing lease from iface. It returns an error if
// there is no lease equal to l. // there is no lease equal to l.
func (iface *netInterface) removeLease(l *Lease) (err error) { func (iface *netInterface) removeLease(l *Lease) (err error) {
i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC) mk := macToKey(l.HWAddr)
_, found := iface.leases[mk]
if !found { if !found {
return fmt.Errorf("no lease for mac %s", l.HWAddr) return fmt.Errorf("no lease for mac %s", l.HWAddr)
} }
iface.leases = slices.Delete(iface.leases, i, i+1) delete(iface.leases, mk)
return nil return nil
} }

View File

@ -1,7 +1,6 @@
package dhcpsvc package dhcpsvc
import ( import (
"bytes"
"net" "net"
"net/netip" "net/netip"
"slices" "slices"
@ -45,8 +44,3 @@ func (l *Lease) Clone() (clone *Lease) {
IsStatic: l.IsStatic, IsStatic: l.IsStatic,
} }
} }
// compareLeaseMAC compares two [Lease]s by hardware address.
func compareLeaseMAC(a, b *Lease) (res int) {
return bytes.Compare(a.HWAddr, b.HWAddr)
}

View File

@ -61,7 +61,7 @@ func (idx *leaseIndex) add(l *Lease, iface *netInterface) (err error) {
return fmt.Errorf("lease for hostname %s already exists", l.Hostname) return fmt.Errorf("lease for hostname %s already exists", l.Hostname)
} }
err = iface.insertLease(l) err = iface.addLease(l)
if err != nil { if err != nil {
return err return err
} }

View File

@ -41,10 +41,10 @@ type DHCPServer struct {
leases *leaseIndex leases *leaseIndex
// interfaces4 is the set of IPv4 interfaces sorted by interface name. // interfaces4 is the set of IPv4 interfaces sorted by interface name.
interfaces4 netInterfacesV4 interfaces4 dhcpInterfacesV4
// interfaces6 is the set of IPv6 interfaces sorted by interface name. // interfaces6 is the set of IPv6 interfaces sorted by interface name.
interfaces6 netInterfacesV6 interfaces6 dhcpInterfacesV6
// icmpTimeout is the timeout for checking another DHCP server's presence. // icmpTimeout is the timeout for checking another DHCP server's presence.
icmpTimeout time.Duration icmpTimeout time.Duration
@ -63,28 +63,9 @@ func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) {
return nil, nil return nil, nil
} }
// TODO(e.burkov): Add validations scoped to the network interfaces set. ifaces4, ifaces6, err := newInterfaces(ctx, l, conf.Interfaces)
ifaces4 := make(netInterfacesV4, 0, len(conf.Interfaces)) if err != nil {
ifaces6 := make(netInterfacesV6, 0, len(conf.Interfaces)) // Don't wrap the error since it's informative enough as is.
var errs []error
mapsutil.SortedRange(conf.Interfaces, func(name string, iface *InterfaceConfig) (cont bool) {
var i4 *netInterfaceV4
i4, err = newNetInterfaceV4(ctx, l, name, iface.IPv4)
if err != nil {
errs = append(errs, fmt.Errorf("interface %q: ipv4: %w", name, err))
} else if i4 != nil {
ifaces4 = append(ifaces4, i4)
}
i6 := newNetInterfaceV6(ctx, l, name, iface.IPv6)
if i6 != nil {
ifaces6 = append(ifaces6, i6)
}
return true
})
if err = errors.Join(errs...); err != nil {
return nil, err return nil, err
} }
@ -112,6 +93,43 @@ func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) {
return srv, nil return srv, nil
} }
// newInterfaces creates interfaces for the given map of interface names to
// their configurations.
func newInterfaces(
ctx context.Context,
l *slog.Logger,
ifaces map[string]*InterfaceConfig,
) (v4 dhcpInterfacesV4, v6 dhcpInterfacesV6, err error) {
defer func() { err = errors.Annotate(err, "creating interfaces: %w") }()
// TODO(e.burkov): Add validations scoped to the network interfaces set.
v4 = make(dhcpInterfacesV4, 0, len(ifaces))
v6 = make(dhcpInterfacesV6, 0, len(ifaces))
var errs []error
mapsutil.SortedRange(ifaces, func(name string, iface *InterfaceConfig) (cont bool) {
var i4 *dhcpInterfaceV4
i4, err = newDHCPInterfaceV4(ctx, l, name, iface.IPv4)
if err != nil {
errs = append(errs, fmt.Errorf("interface %q: ipv4: %w", name, err))
} else if i4 != nil {
v4 = append(v4, i4)
}
i6 := newDHCPInterfaceV6(ctx, l, name, iface.IPv6)
if i6 != nil {
v6 = append(v6, i6)
}
return true
})
if err = errors.Join(errs...); err != nil {
return nil, nil, err
}
return v4, v6, nil
}
// type check // type check
// //
// TODO(e.burkov): Uncomment when the [Interface] interface is implemented. // TODO(e.burkov): Uncomment when the [Interface] interface is implemented.
@ -127,16 +145,11 @@ func (srv *DHCPServer) Leases() (leases []*Lease) {
srv.leasesMu.RLock() srv.leasesMu.RLock()
defer srv.leasesMu.RUnlock() defer srv.leasesMu.RUnlock()
for _, iface := range srv.interfaces4 { srv.leases.rangeLeases(func(l *Lease) (cont bool) {
for _, lease := range iface.leases { leases = append(leases, l.Clone())
leases = append(leases, lease.Clone())
} return true
} })
for _, iface := range srv.interfaces6 {
for _, lease := range iface.leases {
leases = append(leases, lease.Clone())
}
}
return leases return leases
} }
@ -200,10 +213,10 @@ func (srv *DHCPServer) Reset(ctx context.Context) (err error) {
// expects the DHCPServer.leasesMu to be locked. // expects the DHCPServer.leasesMu to be locked.
func (srv *DHCPServer) resetLeases() { func (srv *DHCPServer) resetLeases() {
for _, iface := range srv.interfaces4 { for _, iface := range srv.interfaces4 {
iface.reset() iface.common.reset()
} }
for _, iface := range srv.interfaces6 { for _, iface := range srv.interfaces6 {
iface.reset() iface.common.reset()
} }
srv.leases.clear() srv.leases.clear()
} }

View File

@ -122,7 +122,7 @@ func TestNew(t *testing.T) {
DBFilePath: leasesPath, DBFilePath: leasesPath,
}, },
name: "gateway_within_range", name: "gateway_within_range",
wantErrMsg: `interface "eth0": ipv4: ` + wantErrMsg: `creating interfaces: interface "eth0": ipv4: ` +
`gateway ip 192.168.0.100 in the ip range 192.168.0.1-192.168.0.254`, `gateway ip 192.168.0.100 in the ip range 192.168.0.1-192.168.0.254`,
}, { }, {
conf: &dhcpsvc.Config{ conf: &dhcpsvc.Config{
@ -138,7 +138,7 @@ func TestNew(t *testing.T) {
DBFilePath: leasesPath, DBFilePath: leasesPath,
}, },
name: "bad_start", name: "bad_start",
wantErrMsg: `interface "eth0": ipv4: ` + wantErrMsg: `creating interfaces: interface "eth0": ipv4: ` +
`range start 127.0.0.1 is not within 192.168.0.1/24`, `range start 127.0.0.1 is not within 192.168.0.1/24`,
}} }}
@ -568,5 +568,5 @@ func TestServer_Leases(t *testing.T) {
HWAddr: mustParseMAC(t, "BB:BB:BB:BB:BB:BB"), HWAddr: mustParseMAC(t, "BB:BB:BB:BB:BB:BB"),
IsStatic: true, IsStatic: true,
}} }}
assert.Equal(t, wantLeases, srv.Leases()) assert.ElementsMatch(t, wantLeases, srv.Leases())
} }

View File

@ -82,8 +82,12 @@ func (c *IPv4Config) validate() (err error) {
return errors.Join(errs...) return errors.Join(errs...)
} }
// netInterfaceV4 is a DHCP interface for IPv4 address family. // dhcpInterfaceV4 is a DHCP interface for IPv4 address family.
type netInterfaceV4 struct { 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 is the IP address of the network gateway.
gateway netip.Addr gateway netip.Addr
@ -101,21 +105,17 @@ type netInterfaceV4 struct {
// explicitOpts are the user-configured options. It must not have // explicitOpts are the user-configured options. It must not have
// intersections with implicitOpts. // intersections with implicitOpts.
explicitOpts layers.DHCPOptions explicitOpts layers.DHCPOptions
// netInterface is embedded here to provide some common network interface
// logic.
netInterface
} }
// newNetInterfaceV4 creates a new DHCP interface for IPv4 address family with // newDHCPInterfaceV4 creates a new DHCP interface for IPv4 address family with
// the given configuration. It returns an error if the given configuration // the given configuration. It returns an error if the given configuration
// can't be used. // can't be used.
func newNetInterfaceV4( func newDHCPInterfaceV4(
ctx context.Context, ctx context.Context,
l *slog.Logger, l *slog.Logger,
name string, name string,
conf *IPv4Config, conf *IPv4Config,
) (i *netInterfaceV4, err error) { ) (i *dhcpInterfaceV4, err error) {
l = l.With( l = l.With(
keyInterface, name, keyInterface, name,
keyFamily, netutil.AddrFamilyIPv4, keyFamily, netutil.AddrFamilyIPv4,
@ -144,35 +144,31 @@ func newNetInterfaceV4(
return nil, fmt.Errorf("gateway ip %s in the ip range %s", conf.GatewayIP, addrSpace) return nil, fmt.Errorf("gateway ip %s in the ip range %s", conf.GatewayIP, addrSpace)
} }
i = &netInterfaceV4{ i = &dhcpInterfaceV4{
gateway: conf.GatewayIP, gateway: conf.GatewayIP,
subnet: subnet, subnet: subnet,
addrSpace: addrSpace, addrSpace: addrSpace,
netInterface: netInterface{ common: newNetInterface(name, l, conf.LeaseDuration),
name: name,
leaseTTL: conf.LeaseDuration,
logger: l,
},
} }
i.implicitOpts, i.explicitOpts = conf.options(ctx, l) i.implicitOpts, i.explicitOpts = conf.options(ctx, l)
return i, nil return i, nil
} }
// netInterfacesV4 is a slice of network interfaces of IPv4 address family. // dhcpInterfacesV4 is a slice of network interfaces of IPv4 address family.
type netInterfacesV4 []*netInterfaceV4 type dhcpInterfacesV4 []*dhcpInterfaceV4
// find returns the first network interface within ifaces containing ip. It // find returns the first network interface within ifaces containing ip. It
// returns false if there is no such interface. // returns false if there is no such interface.
func (ifaces netInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok bool) { func (ifaces dhcpInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok bool) {
i := slices.IndexFunc(ifaces, func(iface *netInterfaceV4) (contains bool) { i := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV4) (contains bool) {
return iface.subnet.Contains(ip) return iface.subnet.Contains(ip)
}) })
if i < 0 { if i < 0 {
return nil, false return nil, false
} }
return &ifaces[i].netInterface, true return ifaces[i].common, true
} }
// options returns the implicit and explicit options for the interface. The two // options returns the implicit and explicit options for the interface. The two

View File

@ -62,10 +62,12 @@ func (c *IPv6Config) validate() (err error) {
return errors.Join(errs...) return errors.Join(errs...)
} }
// netInterfaceV6 is a DHCP interface for IPv6 address family. // dhcpInterfaceV6 is a DHCP interface for IPv6 address family.
// type dhcpInterfaceV6 struct {
// TODO(e.burkov): Add options. // common is the common part of any network interface within the DHCP
type netInterfaceV6 struct { // server.
common *netInterface
// rangeStart is the first IP address in the range. // rangeStart is the first IP address in the range.
rangeStart netip.Addr rangeStart netip.Addr
@ -78,10 +80,6 @@ type netInterfaceV6 struct {
// intersections with implicitOpts. // intersections with implicitOpts.
explicitOpts layers.DHCPv6Options explicitOpts layers.DHCPv6Options
// netInterface is embedded here to provide some common network interface
// logic.
netInterface
// raSLAACOnly defines if DHCP should send ICMPv6.RA packets without MO // raSLAACOnly defines if DHCP should send ICMPv6.RA packets without MO
// flags. // flags.
raSLAACOnly bool raSLAACOnly bool
@ -90,16 +88,16 @@ type netInterfaceV6 struct {
raAllowSLAAC bool raAllowSLAAC bool
} }
// newNetInterfaceV6 creates a new DHCP interface for IPv6 address family with // newDHCPInterfaceV6 creates a new DHCP interface for IPv6 address family with
// the given configuration. // the given configuration.
// //
// TODO(e.burkov): Validate properly. // TODO(e.burkov): Validate properly.
func newNetInterfaceV6( func newDHCPInterfaceV6(
ctx context.Context, ctx context.Context,
l *slog.Logger, l *slog.Logger,
name string, name string,
conf *IPv6Config, conf *IPv6Config,
) (i *netInterfaceV6) { ) (i *dhcpInterfaceV6) {
l = l.With(keyInterface, name, keyFamily, netutil.AddrFamilyIPv6) l = l.With(keyInterface, name, keyFamily, netutil.AddrFamilyIPv6)
if !conf.Enabled { if !conf.Enabled {
l.DebugContext(ctx, "disabled") l.DebugContext(ctx, "disabled")
@ -107,13 +105,9 @@ func newNetInterfaceV6(
return nil return nil
} }
i = &netInterfaceV6{ i = &dhcpInterfaceV6{
rangeStart: conf.RangeStart, rangeStart: conf.RangeStart,
netInterface: netInterface{ common: newNetInterface(name, l, conf.LeaseDuration),
name: name,
leaseTTL: conf.LeaseDuration,
logger: l,
},
raSLAACOnly: conf.RASLAACOnly, raSLAACOnly: conf.RASLAACOnly,
raAllowSLAAC: conf.RAAllowSLAAC, raAllowSLAAC: conf.RAAllowSLAAC,
} }
@ -122,12 +116,12 @@ func newNetInterfaceV6(
return i return i
} }
// netInterfacesV4 is a slice of network interfaces of IPv4 address family. // dhcpInterfacesV6 is a slice of network interfaces of IPv6 address family.
type netInterfacesV6 []*netInterfaceV6 type dhcpInterfacesV6 []*dhcpInterfaceV6
// find returns the first network interface within ifaces containing ip. It // find returns the first network interface within ifaces containing ip. It
// returns false if there is no such interface. // returns false if there is no such interface.
func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool) { func (ifaces dhcpInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool) {
// prefLen is the length of prefix to match ip against. // prefLen is the length of prefix to match ip against.
// //
// TODO(e.burkov): DHCPv6 inherits the weird behavior of legacy // TODO(e.burkov): DHCPv6 inherits the weird behavior of legacy
@ -136,7 +130,7 @@ func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool
// be used instead. // be used instead.
const prefLen = netutil.IPv6BitLen - 8 const prefLen = netutil.IPv6BitLen - 8
i := slices.IndexFunc(ifaces, func(iface *netInterfaceV6) (contains bool) { i := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV6) (contains bool) {
return !ip.Less(iface.rangeStart) && return !ip.Less(iface.rangeStart) &&
netip.PrefixFrom(iface.rangeStart, prefLen).Contains(ip) netip.PrefixFrom(iface.rangeStart, prefLen).Contains(ip)
}) })
@ -144,7 +138,7 @@ func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool
return nil, false return nil, false
} }
return &ifaces[i].netInterface, true return ifaces[i].common, true
} }
// options returns the implicit and explicit options for the interface. The two // options returns the implicit and explicit options for the interface. The two