mirror of
https://git.vectorsigma.ru/public/AdGuardHome.git
synced 2026-07-29 07:29:02 +00:00
Pull request #2422: 4923 gopacket dhcp vol.10
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
This commit is contained in:
@@ -10,31 +10,77 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/validate"
|
||||
)
|
||||
|
||||
// Config is the configuration for the DHCP service.
|
||||
type Config struct {
|
||||
// Interfaces stores configurations of DHCP server specific for the network
|
||||
// interface identified by its name.
|
||||
// interface identified by its name. It must not be empty and must only
|
||||
// contain valid interface names and configurations.
|
||||
Interfaces map[string]*InterfaceConfig
|
||||
|
||||
// Logger will be used to log the DHCP events.
|
||||
// Logger will be used to log the DHCP events. It must not be nil.
|
||||
Logger *slog.Logger
|
||||
|
||||
// LocalDomainName is the top-level domain name to use for resolving DHCP
|
||||
// clients' hostnames.
|
||||
// clients' hostnames. It must be a valid domain name.
|
||||
LocalDomainName string
|
||||
|
||||
// DBFilePath is the path to the database file containing the DHCP leases.
|
||||
// It must not be empty.
|
||||
DBFilePath string
|
||||
|
||||
// ICMPTimeout is the timeout for checking another DHCP server's presence.
|
||||
// It must be non-negative. If it is zero, the check will be skipped.
|
||||
ICMPTimeout time.Duration
|
||||
|
||||
// Enabled is the state of the service, whether it is enabled or not.
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ validate.Interface = (*Config)(nil)
|
||||
|
||||
// Validate implements the [validate.Interface] for *Config.
|
||||
func (conf *Config) Validate() (err error) {
|
||||
switch {
|
||||
case conf == nil:
|
||||
return errors.ErrNoValue
|
||||
case !conf.Enabled:
|
||||
return nil
|
||||
}
|
||||
|
||||
errs := []error{
|
||||
validate.NotNegative("ICMPTimeout", conf.ICMPTimeout),
|
||||
}
|
||||
|
||||
err = netutil.ValidateDomainName(conf.LocalDomainName)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("LocalDomainName: %w", err))
|
||||
}
|
||||
|
||||
// This is a best-effort check for the file accessibility. The file will be
|
||||
// checked again when it is opened later.
|
||||
if _, err = os.Stat(conf.DBFilePath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
errs = append(errs, fmt.Errorf("DBFilePath %q: %w", conf.DBFilePath, err))
|
||||
}
|
||||
|
||||
if len(conf.Interfaces) == 0 {
|
||||
err = fmt.Errorf("interfaces: %w", errors.ErrEmptyValue)
|
||||
errs = append(errs, err)
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
for _, iface := range slices.Sorted(maps.Keys(conf.Interfaces)) {
|
||||
ifaceConf := conf.Interfaces[iface]
|
||||
errs = validate.Append(errs, iface, ifaceConf)
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// InterfaceConfig is the configuration of a single DHCP interface.
|
||||
type InterfaceConfig struct {
|
||||
// IPv4 is the configuration of DHCP protocol for IPv4.
|
||||
@@ -44,65 +90,17 @@ type InterfaceConfig struct {
|
||||
IPv6 *IPv6Config
|
||||
}
|
||||
|
||||
// Validate returns an error in conf if any.
|
||||
//
|
||||
// TODO(e.burkov): Unexport and rewrite the test.
|
||||
func (conf *Config) Validate() (err error) {
|
||||
switch {
|
||||
case conf == nil:
|
||||
return errNilConfig
|
||||
case !conf.Enabled:
|
||||
return nil
|
||||
}
|
||||
// type check
|
||||
var _ validate.Interface = (*InterfaceConfig)(nil)
|
||||
|
||||
var errs []error
|
||||
if conf.ICMPTimeout < 0 {
|
||||
err = newMustErr("icmp timeout", "be non-negative", conf.ICMPTimeout)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
err = netutil.ValidateDomainName(conf.LocalDomainName)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
// This is a best-effort check for the file accessibility. The file will be
|
||||
// checked again when it is opened later.
|
||||
if _, err = os.Stat(conf.DBFilePath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
errs = append(errs, fmt.Errorf("db file path %q: %w", conf.DBFilePath, err))
|
||||
}
|
||||
|
||||
if len(conf.Interfaces) == 0 {
|
||||
errs = append(errs, errNoInterfaces)
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
for _, iface := range slices.Sorted(maps.Keys(conf.Interfaces)) {
|
||||
ic := conf.Interfaces[iface]
|
||||
err = ic.validate()
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("interface %q: %w", iface, err))
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// validate returns an error in ic, if any.
|
||||
func (ic *InterfaceConfig) validate() (err error) {
|
||||
// Validate implements the [validate.Interface] interface for *InterfaceConfig.
|
||||
func (ic *InterfaceConfig) Validate() (err error) {
|
||||
if ic == nil {
|
||||
return errNilConfig
|
||||
return errors.ErrNoValue
|
||||
}
|
||||
|
||||
if err = ic.IPv4.validate(); err != nil {
|
||||
return fmt.Errorf("ipv4: %w", err)
|
||||
}
|
||||
|
||||
if err = ic.IPv6.validate(); err != nil {
|
||||
return fmt.Errorf("ipv6: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return errors.Join(
|
||||
errors.Annotate(ic.IPv4.Validate(), "ipv4: %w"),
|
||||
errors.Annotate(ic.IPv6.Validate(), "ipv6: %w"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,49 @@
|
||||
package dhcpsvc_test
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
)
|
||||
|
||||
func TestConfig_Validate(t *testing.T) {
|
||||
validIPv4Conf := &dhcpsvc.IPv4Config{
|
||||
Enabled: true,
|
||||
GatewayIP: netip.MustParseAddr("192.168.0.1"),
|
||||
SubnetMask: netip.MustParseAddr("255.255.255.0"),
|
||||
RangeStart: netip.MustParseAddr("192.168.0.2"),
|
||||
RangeEnd: netip.MustParseAddr("192.168.0.254"),
|
||||
LeaseDuration: 1 * time.Hour,
|
||||
}
|
||||
gwInRangeConf := &dhcpsvc.IPv4Config{
|
||||
Enabled: true,
|
||||
GatewayIP: netip.MustParseAddr("192.168.0.100"),
|
||||
SubnetMask: netip.MustParseAddr("255.255.255.0"),
|
||||
RangeStart: netip.MustParseAddr("192.168.0.1"),
|
||||
RangeEnd: netip.MustParseAddr("192.168.0.254"),
|
||||
LeaseDuration: 1 * time.Hour,
|
||||
}
|
||||
badStartConf := &dhcpsvc.IPv4Config{
|
||||
Enabled: true,
|
||||
GatewayIP: netip.MustParseAddr("192.168.0.1"),
|
||||
SubnetMask: netip.MustParseAddr("255.255.255.0"),
|
||||
RangeStart: netip.MustParseAddr("127.0.0.1"),
|
||||
RangeEnd: netip.MustParseAddr("192.168.0.254"),
|
||||
LeaseDuration: 1 * time.Hour,
|
||||
}
|
||||
|
||||
validIPv6Conf := &dhcpsvc.IPv6Config{
|
||||
Enabled: true,
|
||||
RangeStart: netip.MustParseAddr("2001:db8::1"),
|
||||
LeaseDuration: 1 * time.Hour,
|
||||
RAAllowSLAAC: true,
|
||||
RASLAACOnly: true,
|
||||
}
|
||||
|
||||
leasesPath := filepath.Join(t.TempDir(), "leases.json")
|
||||
|
||||
testCases := []struct {
|
||||
@@ -18,7 +53,7 @@ func TestConfig_Validate(t *testing.T) {
|
||||
}{{
|
||||
name: "nil_config",
|
||||
conf: nil,
|
||||
wantErrMsg: "config is nil",
|
||||
wantErrMsg: "no value",
|
||||
}, {
|
||||
name: "disabled",
|
||||
conf: &dhcpsvc.Config{},
|
||||
@@ -30,7 +65,7 @@ func TestConfig_Validate(t *testing.T) {
|
||||
Interfaces: testInterfaceConf,
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
wantErrMsg: `bad domain name "": domain name is empty`,
|
||||
wantErrMsg: `LocalDomainName: bad domain name "": domain name is empty`,
|
||||
}, {
|
||||
conf: &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
@@ -39,16 +74,7 @@ func TestConfig_Validate(t *testing.T) {
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "no_interfaces",
|
||||
wantErrMsg: "no interfaces specified",
|
||||
}, {
|
||||
conf: &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: nil,
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "no_interfaces",
|
||||
wantErrMsg: "no interfaces specified",
|
||||
wantErrMsg: "interfaces: empty value",
|
||||
}, {
|
||||
conf: &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
@@ -59,7 +85,7 @@ func TestConfig_Validate(t *testing.T) {
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "nil_interface",
|
||||
wantErrMsg: `interface "eth0": config is nil`,
|
||||
wantErrMsg: `eth0: no value`,
|
||||
}, {
|
||||
conf: &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
@@ -73,7 +99,7 @@ func TestConfig_Validate(t *testing.T) {
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "nil_ipv4",
|
||||
wantErrMsg: `interface "eth0": ipv4: config is nil`,
|
||||
wantErrMsg: `eth0: ipv4: no value`,
|
||||
}, {
|
||||
conf: &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
@@ -87,7 +113,69 @@ func TestConfig_Validate(t *testing.T) {
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "nil_ipv6",
|
||||
wantErrMsg: `interface "eth0": ipv6: config is nil`,
|
||||
wantErrMsg: `eth0: ipv6: no value`,
|
||||
}, {
|
||||
conf: &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
Logger: discardLog,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: map[string]*dhcpsvc.InterfaceConfig{
|
||||
"eth0": {
|
||||
IPv4: validIPv4Conf,
|
||||
IPv6: validIPv6Conf,
|
||||
},
|
||||
},
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "valid",
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
conf: &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
Logger: discardLog,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: map[string]*dhcpsvc.InterfaceConfig{
|
||||
"eth0": {
|
||||
IPv4: &dhcpsvc.IPv4Config{Enabled: false},
|
||||
IPv6: &dhcpsvc.IPv6Config{Enabled: false},
|
||||
},
|
||||
},
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "disabled_interfaces",
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
conf: &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
Logger: discardLog,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: map[string]*dhcpsvc.InterfaceConfig{
|
||||
"eth0": {
|
||||
IPv4: gwInRangeConf,
|
||||
IPv6: validIPv6Conf,
|
||||
},
|
||||
},
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "gateway_within_range",
|
||||
wantErrMsg: "eth0: ipv4: gateway ip 192.168.0.100 in the ip range " +
|
||||
"192.168.0.1-192.168.0.254",
|
||||
}, {
|
||||
conf: &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
Logger: discardLog,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: map[string]*dhcpsvc.InterfaceConfig{
|
||||
"eth0": {
|
||||
IPv4: badStartConf,
|
||||
IPv6: validIPv6Conf,
|
||||
},
|
||||
},
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "bad_start",
|
||||
wantErrMsg: "eth0: ipv4: range start 127.0.0.1 is not within 192.168.0.1/24" + "\n" +
|
||||
"gateway ip 192.168.0.1 in the ip range 127.0.0.1-192.168.0.254",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -2,20 +2,12 @@ package dhcpsvc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// errNilConfig is returned when a nil config met.
|
||||
errNilConfig errors.Error = "config is nil"
|
||||
|
||||
// errNoInterfaces is returned when no interfaces found in configuration.
|
||||
errNoInterfaces errors.Error = "no interfaces specified"
|
||||
)
|
||||
|
||||
// newMustErr returns an error that indicates that valName must be as must
|
||||
// describes.
|
||||
//
|
||||
// TODO(e.burkov): Use [validate] and remove this function.
|
||||
func newMustErr(valName, must string, val fmt.Stringer) (err error) {
|
||||
return fmt.Errorf("%s %s must %s", valName, val, must)
|
||||
}
|
||||
|
||||
61
internal/dhcpsvc/handle.go
Normal file
61
internal/dhcpsvc/handle.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package dhcpsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
// responseWriter4 writes DHCPv4 response to the client.
|
||||
type responseWriter4 interface {
|
||||
// write writes the DHCPv4 response to the client.
|
||||
write(ctx context.Context, pkt *layers.DHCPv4) (err error)
|
||||
}
|
||||
|
||||
// responseWriter6 writes DHCPv6 response to the client.
|
||||
type responseWriter6 interface {
|
||||
// write writes the DHCPv6 response to the client.
|
||||
write(ctx context.Context, pkt *layers.DHCPv6) (err error)
|
||||
}
|
||||
|
||||
// serve handles the incoming packets and dispatches them to the appropriate
|
||||
// handler based on the Ethernet type. It's used to run in a separate goroutine
|
||||
// as it blocks until packets channel is closed.
|
||||
func (srv *DHCPServer) serve(ctx context.Context) {
|
||||
defer slogutil.RecoverAndLog(ctx, srv.logger)
|
||||
|
||||
for pkt := range srv.packetSource.Packets() {
|
||||
etherLayer, ok := pkt.Layer(layers.LayerTypeEthernet).(*layers.Ethernet)
|
||||
if !ok {
|
||||
actual := pkt.Layers()
|
||||
srv.logger.DebugContext(ctx, "skipping non-ethernet packet", "layers", actual)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
switch typ := etherLayer.EthernetType; typ {
|
||||
case layers.EthernetTypeIPv4:
|
||||
// TODO(e.burkov): Set the response writer.
|
||||
var rw responseWriter4
|
||||
err = srv.serveV4(ctx, rw, pkt)
|
||||
case layers.EthernetTypeIPv6:
|
||||
// TODO(e.burkov): Set the response writer.
|
||||
var rw responseWriter6
|
||||
err = srv.serveV6(ctx, rw, pkt)
|
||||
default:
|
||||
// TODO(e.burkov): It seems, there is another standard for Ethernet
|
||||
// header, which uses the Length field instead of the EthernetType,
|
||||
// so handle it properly.
|
||||
srv.logger.DebugContext(ctx, "skipping ethernet packet", "type", typ)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
srv.logger.ErrorContext(ctx, "serving", slogutil.KeyError, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
130
internal/dhcpsvc/handler4.go
Normal file
130
internal/dhcpsvc/handler4.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package dhcpsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
// serveV4 handles the ethernet packet of IPv4 type. rw and pkt must not be
|
||||
// nil.
|
||||
func (srv *DHCPServer) serveV4(
|
||||
ctx context.Context,
|
||||
rw responseWriter4,
|
||||
pkt gopacket.Packet,
|
||||
) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "serving dhcpv4: %w") }()
|
||||
|
||||
req, ok := pkt.Layer(layers.LayerTypeDHCPv4).(*layers.DHCPv4)
|
||||
if !ok {
|
||||
// TODO(e.burkov): Consider adding some debug information about the
|
||||
// actual received packet.
|
||||
srv.logger.DebugContext(ctx, "skipping non-dhcpv4 packet")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Handle duplicate Xid.
|
||||
|
||||
if req.Operation != layers.DHCPOpRequest {
|
||||
srv.logger.DebugContext(ctx, "skipping non-request dhcpv4 packet", "op", req.Operation)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
typ, ok := msg4Type(req)
|
||||
if !ok {
|
||||
// The "DHCP message type" option - must be included in every DHCP
|
||||
// message.
|
||||
//
|
||||
// See https://datatracker.ietf.org/doc/html/rfc2131#section-3.
|
||||
return fmt.Errorf("dhcpv4: message type: %w", errors.ErrNoValue)
|
||||
}
|
||||
|
||||
return srv.handleDHCPv4(ctx, rw, typ, req)
|
||||
}
|
||||
|
||||
// handleDHCPv4 handles the DHCPv4 message of the given type.
|
||||
func (srv *DHCPServer) handleDHCPv4(
|
||||
ctx context.Context,
|
||||
rw responseWriter4,
|
||||
typ layers.DHCPMsgType,
|
||||
req *layers.DHCPv4,
|
||||
) (err error) {
|
||||
// Each interface should handle the DISCOVER and REQUEST messages offer and
|
||||
// allocate the available leases. The RELEASE and DECLINE messages should
|
||||
// be handled by the server itself as it should remove the lease.
|
||||
switch typ {
|
||||
case layers.DHCPMsgTypeDiscover:
|
||||
srv.handleDiscover(ctx, rw, req)
|
||||
case layers.DHCPMsgTypeRequest:
|
||||
srv.handleRequest(ctx, rw, req)
|
||||
case layers.DHCPMsgTypeRelease:
|
||||
// TODO(e.burkov): Remove the lease, either allocated or offered.
|
||||
case layers.DHCPMsgTypeDecline:
|
||||
// TODO(e.burkov): Remove the allocated lease. RFC tells it only
|
||||
// possible if the client found the address already in use.
|
||||
default:
|
||||
// TODO(e.burkov): Handle DHCPINFORM.
|
||||
return fmt.Errorf("dhcpv4: request type: %w: %v", errors.ErrBadEnumValue, typ)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleDiscover handles the DHCPv4 message of discover type.
|
||||
func (srv *DHCPServer) handleDiscover(ctx context.Context, rw responseWriter4, req *layers.DHCPv4) {
|
||||
// TODO(e.burkov): Check existing leases, either allocated or offered.
|
||||
|
||||
for _, iface := range srv.interfaces4 {
|
||||
go iface.handleDiscover(ctx, rw, req)
|
||||
}
|
||||
}
|
||||
|
||||
// handleRequest handles the DHCPv4 message of request type.
|
||||
func (srv *DHCPServer) handleRequest(ctx context.Context, rw responseWriter4, req *layers.DHCPv4) {
|
||||
srvID, hasSrvID := serverID4(req)
|
||||
reqIP, hasReqIP := requestedIPv4(req)
|
||||
|
||||
switch {
|
||||
case hasSrvID && !srvID.IsUnspecified():
|
||||
// If the DHCPREQUEST message contains a server identifier option, the
|
||||
// message is in response to a DHCPOFFER message. Otherwise, the
|
||||
// message is a request to verify or extend an existing lease.
|
||||
iface, hasIface := srv.interfaces4.findInterface(srvID)
|
||||
if !hasIface {
|
||||
srv.logger.DebugContext(ctx, "skipping selecting request", "serverid", srvID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
iface.handleSelecting(ctx, rw, req, reqIP)
|
||||
case hasReqIP && !reqIP.IsUnspecified():
|
||||
// Requested IP address option MUST be filled in with client's notion of
|
||||
// its previously assigned address.
|
||||
iface, hasIface := srv.interfaces4.findInterface(reqIP)
|
||||
if !hasIface {
|
||||
srv.logger.DebugContext(ctx, "skipping init-reboot request", "requestedip", reqIP)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
iface.handleInitReboot(ctx, rw, req, reqIP)
|
||||
default:
|
||||
// Server identifier MUST NOT be filled in, requested IP address option
|
||||
// MUST NOT be filled in.
|
||||
ip, _ := netip.AddrFromSlice(req.ClientIP.To4())
|
||||
iface, hasIface := srv.interfaces4.findInterface(ip)
|
||||
if !hasIface {
|
||||
srv.logger.DebugContext(ctx, "skipping init-reboot request", "clientip", ip)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
iface.handleRenew(ctx, rw, req)
|
||||
}
|
||||
}
|
||||
58
internal/dhcpsvc/handler6.go
Normal file
58
internal/dhcpsvc/handler6.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package dhcpsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
// serveV6 handles the ethernet packet of IPv6 type. rw and pkt must not be
|
||||
// nil.
|
||||
func (srv *DHCPServer) serveV6(
|
||||
ctx context.Context,
|
||||
rw responseWriter6,
|
||||
pkt gopacket.Packet,
|
||||
) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "serving dhcpv6: %w") }()
|
||||
|
||||
msg, ok := pkt.Layer(layers.LayerTypeDHCPv6).(*layers.DHCPv6)
|
||||
if !ok {
|
||||
// TODO(e.burkov): Consider adding some debug information about the
|
||||
// actual received packet.
|
||||
srv.logger.DebugContext(ctx, "skipping non-dhcpv6 packet")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Handle duplicate TransactionID.
|
||||
|
||||
return srv.handleDHCPv6(ctx, rw, msg.MsgType, msg)
|
||||
}
|
||||
|
||||
// handleDHCPv6 handles the DHCPv6 message of the given type.
|
||||
func (srv *DHCPServer) handleDHCPv6(
|
||||
_ context.Context,
|
||||
_ responseWriter6,
|
||||
typ layers.DHCPv6MsgType,
|
||||
_ *layers.DHCPv6,
|
||||
) (err error) {
|
||||
switch typ {
|
||||
case
|
||||
layers.DHCPv6MsgTypeSolicit,
|
||||
layers.DHCPv6MsgTypeRequest,
|
||||
layers.DHCPv6MsgTypeConfirm,
|
||||
layers.DHCPv6MsgTypeRenew,
|
||||
layers.DHCPv6MsgTypeRebind,
|
||||
layers.DHCPv6MsgTypeInformationRequest,
|
||||
layers.DHCPv6MsgTypeRelease,
|
||||
layers.DHCPv6MsgTypeDecline:
|
||||
// TODO(e.burkov): Handle messages.
|
||||
default:
|
||||
return fmt.Errorf("dhcpv6: request type: %w: %v", errors.ErrBadEnumValue, typ)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -45,17 +45,6 @@ type netInterface struct {
|
||||
leaseTTL time.Duration
|
||||
}
|
||||
|
||||
// newNetInterface creates a new netInterface with the given name, leaseTTL, and
|
||||
// logger.
|
||||
func newNetInterface(name string, l *slog.Logger, leaseTTL time.Duration) (iface *netInterface) {
|
||||
return &netInterface{
|
||||
logger: l,
|
||||
leases: map[macKey]*Lease{},
|
||||
name: name,
|
||||
leaseTTL: leaseTTL,
|
||||
}
|
||||
}
|
||||
|
||||
// reset clears all the slices in iface for reuse.
|
||||
func (iface *netInterface) reset() {
|
||||
clear(iface.leases)
|
||||
|
||||
@@ -13,9 +13,13 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/google/gopacket"
|
||||
)
|
||||
|
||||
// DHCPServer is a DHCP server for both IPv4 and IPv6 address families.
|
||||
//
|
||||
// TODO(e.burkov): Rename to Default.
|
||||
type DHCPServer struct {
|
||||
// enabled indicates whether the DHCP server is enabled and can provide
|
||||
// information about its clients.
|
||||
@@ -24,6 +28,11 @@ type DHCPServer struct {
|
||||
// logger logs common DHCP events.
|
||||
logger *slog.Logger
|
||||
|
||||
// packetSource is the source of DHCP packets to process.
|
||||
//
|
||||
// TODO(e.burkov): Implement and set.
|
||||
packetSource gopacket.PacketSource
|
||||
|
||||
// localTLD is the top-level domain name to use for resolving DHCP clients'
|
||||
// hostnames.
|
||||
localTLD string
|
||||
@@ -51,8 +60,8 @@ type DHCPServer struct {
|
||||
icmpTimeout time.Duration
|
||||
}
|
||||
|
||||
// New creates a new DHCP server with the given configuration. It returns an
|
||||
// error if the given configuration can't be used.
|
||||
// New creates a new DHCP server with the given configuration. conf must be
|
||||
// valid.
|
||||
//
|
||||
// TODO(e.burkov): Use.
|
||||
func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) {
|
||||
@@ -64,11 +73,7 @@ func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ifaces4, ifaces6, err := newInterfaces(ctx, l, conf.Interfaces)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return nil, err
|
||||
}
|
||||
ifaces4, ifaces6 := newInterfaces(ctx, l, conf.Interfaces)
|
||||
|
||||
enabled := &atomic.Bool{}
|
||||
enabled.Store(conf.Enabled)
|
||||
@@ -95,40 +100,42 @@ func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) {
|
||||
}
|
||||
|
||||
// newInterfaces creates interfaces for the given map of interface names to
|
||||
// their configurations.
|
||||
// their configurations. ifaces must be valid, baseLogger must not be nil.
|
||||
func newInterfaces(
|
||||
ctx context.Context,
|
||||
l *slog.Logger,
|
||||
baseLogger *slog.Logger,
|
||||
ifaces map[string]*InterfaceConfig,
|
||||
) (v4 dhcpInterfacesV4, v6 dhcpInterfacesV6, err error) {
|
||||
defer func() { err = errors.Annotate(err, "creating interfaces: %w") }()
|
||||
|
||||
) (v4 dhcpInterfacesV4, v6 dhcpInterfacesV6) {
|
||||
// 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
|
||||
for _, name := range slices.Sorted(maps.Keys(ifaces)) {
|
||||
iface := ifaces[name]
|
||||
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)
|
||||
ifaceLogger := baseLogger.With(keyInterface, name)
|
||||
|
||||
iface4 := newDHCPInterfaceV4(
|
||||
ctx,
|
||||
ifaceLogger.With(keyFamily, netutil.AddrFamilyIPv4),
|
||||
name,
|
||||
iface.IPv4,
|
||||
)
|
||||
if iface4 != nil {
|
||||
v4 = append(v4, iface4)
|
||||
}
|
||||
|
||||
i6 := newDHCPInterfaceV6(ctx, l, name, iface.IPv6)
|
||||
if i6 != nil {
|
||||
v6 = append(v6, i6)
|
||||
iface6 := newDHCPInterfaceV6(
|
||||
ctx,
|
||||
ifaceLogger.With(keyFamily, netutil.AddrFamilyIPv6),
|
||||
name,
|
||||
iface.IPv6,
|
||||
)
|
||||
if iface6 != nil {
|
||||
v6 = append(v6, iface6)
|
||||
}
|
||||
}
|
||||
|
||||
if err = errors.Join(errs...); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return v4, v6, nil
|
||||
return v4, v6
|
||||
}
|
||||
|
||||
// type check
|
||||
@@ -136,6 +143,25 @@ func newInterfaces(
|
||||
// TODO(e.burkov): Uncomment when the [Interface] interface is implemented.
|
||||
// var _ Interface = (*DHCPServer)(nil)
|
||||
|
||||
// Start implements the [Interface] interface for *DHCPServer.
|
||||
func (srv *DHCPServer) Start(ctx context.Context) (err error) {
|
||||
srv.logger.DebugContext(ctx, "starting dhcp server")
|
||||
|
||||
// TODO(e.burkov): Listen to configured interfaces.
|
||||
|
||||
go srv.serve(context.WithoutCancel(ctx))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srv *DHCPServer) Shutdown(ctx context.Context) (err error) {
|
||||
srv.logger.DebugContext(ctx, "shutting down dhcp server")
|
||||
|
||||
// TODO(e.burkov): Close the packet source.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enabled implements the [Interface] interface for *DHCPServer.
|
||||
func (srv *DHCPServer) Enabled() (ok bool) {
|
||||
return srv.enabled.Load()
|
||||
@@ -146,11 +172,9 @@ func (srv *DHCPServer) Leases() (leases []*Lease) {
|
||||
srv.leasesMu.RLock()
|
||||
defer srv.leasesMu.RUnlock()
|
||||
|
||||
srv.leases.rangeLeases(func(l *Lease) (cont bool) {
|
||||
leases = append(leases, l.Clone())
|
||||
|
||||
return true
|
||||
})
|
||||
for l := range srv.leases.rangeLeases {
|
||||
leases = append(leases, l)
|
||||
}
|
||||
|
||||
return leases
|
||||
}
|
||||
@@ -253,7 +277,7 @@ func (srv *DHCPServer) AddLease(ctx context.Context, l *Lease) (err error) {
|
||||
"hostname", l.Hostname,
|
||||
"ip", l.IP,
|
||||
"mac", l.HWAddr,
|
||||
"static", l.IsStatic,
|
||||
"is_static", l.IsStatic,
|
||||
)
|
||||
|
||||
return nil
|
||||
@@ -292,7 +316,7 @@ func (srv *DHCPServer) UpdateStaticLease(ctx context.Context, l *Lease) (err err
|
||||
"hostname", l.Hostname,
|
||||
"ip", l.IP,
|
||||
"mac", l.HWAddr,
|
||||
"static", l.IsStatic,
|
||||
"is_static", l.IsStatic,
|
||||
)
|
||||
|
||||
return nil
|
||||
@@ -329,7 +353,51 @@ func (srv *DHCPServer) RemoveLease(ctx context.Context, l *Lease) (err error) {
|
||||
"hostname", l.Hostname,
|
||||
"ip", l.IP,
|
||||
"mac", l.HWAddr,
|
||||
"static", l.IsStatic,
|
||||
"is_static", l.IsStatic,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeLeaseByAddr removes the lease with the given IP address from the
|
||||
// server. It returns an error if the lease can't be removed.
|
||||
//
|
||||
//lint:ignore U1000 TODO(e.burkov): Use
|
||||
func (srv *DHCPServer) removeLeaseByAddr(ctx context.Context, addr netip.Addr) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "removing lease by address: %w") }()
|
||||
|
||||
iface, err := srv.ifaceForAddr(addr)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's already informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
srv.leasesMu.Lock()
|
||||
defer srv.leasesMu.Unlock()
|
||||
|
||||
l, ok := srv.leases.leaseByAddr(addr)
|
||||
if !ok {
|
||||
return fmt.Errorf("no lease for ip %s", addr)
|
||||
}
|
||||
|
||||
err = srv.leases.remove(l, iface)
|
||||
if err != nil {
|
||||
// Don't wrap the error since there is already an annotation deferred.
|
||||
return err
|
||||
}
|
||||
|
||||
err = srv.dbStore(ctx)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's already informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
iface.logger.DebugContext(
|
||||
ctx, "removed lease",
|
||||
"hostname", l.Hostname,
|
||||
"ip", l.IP,
|
||||
"mac", l.HWAddr,
|
||||
"is_static", l.IsStatic,
|
||||
)
|
||||
|
||||
return nil
|
||||
|
||||
@@ -40,120 +40,6 @@ func newTempDB(tb testing.TB) (dst string) {
|
||||
return dst
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
validIPv4Conf := &dhcpsvc.IPv4Config{
|
||||
Enabled: true,
|
||||
GatewayIP: netip.MustParseAddr("192.168.0.1"),
|
||||
SubnetMask: netip.MustParseAddr("255.255.255.0"),
|
||||
RangeStart: netip.MustParseAddr("192.168.0.2"),
|
||||
RangeEnd: netip.MustParseAddr("192.168.0.254"),
|
||||
LeaseDuration: 1 * time.Hour,
|
||||
}
|
||||
gwInRangeConf := &dhcpsvc.IPv4Config{
|
||||
Enabled: true,
|
||||
GatewayIP: netip.MustParseAddr("192.168.0.100"),
|
||||
SubnetMask: netip.MustParseAddr("255.255.255.0"),
|
||||
RangeStart: netip.MustParseAddr("192.168.0.1"),
|
||||
RangeEnd: netip.MustParseAddr("192.168.0.254"),
|
||||
LeaseDuration: 1 * time.Hour,
|
||||
}
|
||||
badStartConf := &dhcpsvc.IPv4Config{
|
||||
Enabled: true,
|
||||
GatewayIP: netip.MustParseAddr("192.168.0.1"),
|
||||
SubnetMask: netip.MustParseAddr("255.255.255.0"),
|
||||
RangeStart: netip.MustParseAddr("127.0.0.1"),
|
||||
RangeEnd: netip.MustParseAddr("192.168.0.254"),
|
||||
LeaseDuration: 1 * time.Hour,
|
||||
}
|
||||
|
||||
validIPv6Conf := &dhcpsvc.IPv6Config{
|
||||
Enabled: true,
|
||||
RangeStart: netip.MustParseAddr("2001:db8::1"),
|
||||
LeaseDuration: 1 * time.Hour,
|
||||
RAAllowSLAAC: true,
|
||||
RASLAACOnly: true,
|
||||
}
|
||||
|
||||
leasesPath := filepath.Join(t.TempDir(), "leases.json")
|
||||
|
||||
testCases := []struct {
|
||||
conf *dhcpsvc.Config
|
||||
name string
|
||||
wantErrMsg string
|
||||
}{{
|
||||
conf: &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
Logger: discardLog,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: map[string]*dhcpsvc.InterfaceConfig{
|
||||
"eth0": {
|
||||
IPv4: validIPv4Conf,
|
||||
IPv6: validIPv6Conf,
|
||||
},
|
||||
},
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "valid",
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
conf: &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
Logger: discardLog,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: map[string]*dhcpsvc.InterfaceConfig{
|
||||
"eth0": {
|
||||
IPv4: &dhcpsvc.IPv4Config{Enabled: false},
|
||||
IPv6: &dhcpsvc.IPv6Config{Enabled: false},
|
||||
},
|
||||
},
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "disabled_interfaces",
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
conf: &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
Logger: discardLog,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: map[string]*dhcpsvc.InterfaceConfig{
|
||||
"eth0": {
|
||||
IPv4: gwInRangeConf,
|
||||
IPv6: validIPv6Conf,
|
||||
},
|
||||
},
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "gateway_within_range",
|
||||
wantErrMsg: `creating interfaces: interface "eth0": ipv4: ` +
|
||||
`gateway ip 192.168.0.100 in the ip range 192.168.0.1-192.168.0.254`,
|
||||
}, {
|
||||
conf: &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
Logger: discardLog,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: map[string]*dhcpsvc.InterfaceConfig{
|
||||
"eth0": {
|
||||
IPv4: badStartConf,
|
||||
IPv6: validIPv6Conf,
|
||||
},
|
||||
},
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "bad_start",
|
||||
wantErrMsg: `creating interfaces: interface "eth0": ipv4: ` +
|
||||
`range start 127.0.0.1 is not within 192.168.0.1/24`,
|
||||
}}
|
||||
|
||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := dhcpsvc.New(ctx, tc.conf)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDHCPServer_AddLease(t *testing.T) {
|
||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||
|
||||
|
||||
@@ -11,14 +11,16 @@ import (
|
||||
|
||||
"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 in calculating the
|
||||
// network-specific broadcast address.
|
||||
// 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
|
||||
@@ -26,17 +28,22 @@ type IPv4Config struct {
|
||||
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.
|
||||
// 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 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 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.
|
||||
// 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
|
||||
@@ -44,35 +51,23 @@ type IPv4Config struct {
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
// validate returns an error in conf if any.
|
||||
func (c *IPv4Config) validate() (err error) {
|
||||
// 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 errNilConfig
|
||||
return errors.ErrNoValue
|
||||
} else if !c.Enabled {
|
||||
// Don't validate the configuration for disabled interface.
|
||||
return nil
|
||||
}
|
||||
|
||||
var errs []error
|
||||
|
||||
if !c.GatewayIP.Is4() {
|
||||
err = newMustErr("gateway ip", "be a valid ipv4", c.GatewayIP)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if !c.SubnetMask.Is4() {
|
||||
err = newMustErr("subnet mask", "be a valid ipv4 cidr mask", c.SubnetMask)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if !c.RangeStart.Is4() {
|
||||
err = newMustErr("range start", "be a valid ipv4", c.RangeStart)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if !c.RangeEnd.Is4() {
|
||||
err = newMustErr("range end", "be a valid ipv4", c.RangeEnd)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
errs = c.validateSubnet(errs)
|
||||
|
||||
if c.LeaseDuration <= 0 {
|
||||
err = newMustErr("icmp timeout", "be positive", c.LeaseDuration)
|
||||
@@ -82,6 +77,50 @@ func (c *IPv4Config) validate() (err error) {
|
||||
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
|
||||
@@ -91,7 +130,7 @@ type dhcpInterfaceV4 struct {
|
||||
// gateway is the IP address of the network gateway.
|
||||
gateway netip.Addr
|
||||
|
||||
// subnet is the network subnet.
|
||||
// subnet is the network subnet of the interface.
|
||||
subnet netip.Prefix
|
||||
|
||||
// addrSpace is the IPv4 address space allocated for leasing.
|
||||
@@ -108,67 +147,39 @@ type dhcpInterfaceV4 struct {
|
||||
}
|
||||
|
||||
// newDHCPInterfaceV4 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.
|
||||
// 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,
|
||||
) (i *dhcpInterfaceV4, err error) {
|
||||
l = l.With(
|
||||
keyInterface, name,
|
||||
keyFamily, netutil.AddrFamilyIPv4,
|
||||
)
|
||||
|
||||
) (iface *dhcpInterfaceV4) {
|
||||
if !conf.Enabled {
|
||||
l.DebugContext(ctx, "disabled")
|
||||
|
||||
return nil, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Add a helper for converting [netip.Addr] to subnet mask
|
||||
// to [netutil].
|
||||
maskLen, _ := net.IPMask(conf.SubnetMask.AsSlice()).Size()
|
||||
subnet := netip.PrefixFrom(conf.GatewayIP, maskLen)
|
||||
addrSpace, _ := newIPRange(conf.RangeStart, conf.RangeEnd)
|
||||
|
||||
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 = &dhcpInterfaceV4{
|
||||
iface = &dhcpInterfaceV4{
|
||||
gateway: conf.GatewayIP,
|
||||
subnet: subnet,
|
||||
subnet: netip.PrefixFrom(conf.GatewayIP, maskLen),
|
||||
addrSpace: addrSpace,
|
||||
common: newNetInterface(name, l, conf.LeaseDuration),
|
||||
common: &netInterface{
|
||||
logger: l,
|
||||
leases: map[macKey]*Lease{},
|
||||
name: name,
|
||||
leaseTTL: conf.LeaseDuration,
|
||||
},
|
||||
}
|
||||
i.implicitOpts, i.explicitOpts = conf.options(ctx, l)
|
||||
iface.implicitOpts, iface.explicitOpts = conf.options(ctx, l)
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
return iface
|
||||
}
|
||||
|
||||
// options returns the implicit and explicit options for the interface. The two
|
||||
@@ -361,3 +372,106 @@ func (c *IPv4Config) options(ctx context.Context, l *slog.Logger) (imp, exp laye
|
||||
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
|
||||
}
|
||||
|
||||
@@ -10,20 +10,22 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/validate"
|
||||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
// IPv6Config is the interface-specific configuration for DHCPv6.
|
||||
type IPv6Config struct {
|
||||
// RangeStart is the first address in the range to assign to DHCP clients.
|
||||
// It should be a valid IPv6 address.
|
||||
RangeStart netip.Addr
|
||||
|
||||
// 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 is the list of explicit DHCP options to send to 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 is the TTL of a DHCP lease. It should be positive.
|
||||
LeaseDuration time.Duration
|
||||
|
||||
// RASlaacOnly defines whether the DHCP clients should only use SLAAC for
|
||||
@@ -39,10 +41,13 @@ type IPv6Config struct {
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
// validate returns an error in conf if any.
|
||||
func (c *IPv6Config) validate() (err error) {
|
||||
// type check
|
||||
var _ validate.Interface = (*IPv6Config)(nil)
|
||||
|
||||
// Validate implements the [validate.Interface] interface for *IPv6Config.
|
||||
func (c *IPv6Config) Validate() (err error) {
|
||||
if c == nil {
|
||||
return errNilConfig
|
||||
return errors.ErrNoValue
|
||||
} else if !c.Enabled {
|
||||
return nil
|
||||
}
|
||||
@@ -89,31 +94,34 @@ type dhcpInterfaceV6 struct {
|
||||
}
|
||||
|
||||
// newDHCPInterfaceV6 creates a new DHCP interface for IPv6 address family with
|
||||
// the given configuration.
|
||||
//
|
||||
// TODO(e.burkov): Validate properly.
|
||||
// the given configuration. If the interface is disabled, it returns nil. conf
|
||||
// must be valid.
|
||||
func newDHCPInterfaceV6(
|
||||
ctx context.Context,
|
||||
l *slog.Logger,
|
||||
name string,
|
||||
conf *IPv6Config,
|
||||
) (i *dhcpInterfaceV6) {
|
||||
l = l.With(keyInterface, name, keyFamily, netutil.AddrFamilyIPv6)
|
||||
) (iface *dhcpInterfaceV6) {
|
||||
if !conf.Enabled {
|
||||
l.DebugContext(ctx, "disabled")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
i = &dhcpInterfaceV6{
|
||||
rangeStart: conf.RangeStart,
|
||||
common: newNetInterface(name, l, conf.LeaseDuration),
|
||||
iface = &dhcpInterfaceV6{
|
||||
rangeStart: conf.RangeStart,
|
||||
common: &netInterface{
|
||||
logger: l,
|
||||
leases: map[macKey]*Lease{},
|
||||
name: name,
|
||||
leaseTTL: conf.LeaseDuration,
|
||||
},
|
||||
raSLAACOnly: conf.RASLAACOnly,
|
||||
raAllowSLAAC: conf.RAAllowSLAAC,
|
||||
}
|
||||
i.implicitOpts, i.explicitOpts = conf.options(ctx, l)
|
||||
iface.implicitOpts, iface.explicitOpts = conf.options(ctx, l)
|
||||
|
||||
return i
|
||||
return iface
|
||||
}
|
||||
|
||||
// dhcpInterfacesV6 is a slice of network interfaces of IPv6 address family.
|
||||
|
||||
Reference in New Issue
Block a user