Files
DDI/agent/role/dhcp/config.go
T
beug.k@beugsiud.deandClaude Opus 5.5 562f35c10c feat(agent/dhcp): Kea DHCPv4 role over the UNIX control socket
Validate requires exactly kea-dhcp4.conf with this agent's control
socket and runs kea-dhcp4 -t. Activate writes the file atomically and
sends config-reload; on failure the previous file is restored and
reloaded. Health uses status-get (pid, uptime, last reload, socket
state) and checks that the installed file is the expected revision.
Leases pages through lease4-get-page and maps Kea lease states
(default, declined, expired-reclaimed, released) to core lease states.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:14:34 +02:00

76 lines
2.6 KiB
Go

// SPDX-License-Identifier: Apache-2.0
package dhcp
import (
"errors"
"fmt"
"path"
"strings"
"time"
)
// Config is the "dhcp:" section of the agent configuration file.
type Config struct {
// ConfigFile is the file kea-dhcp4 was started with (-c); config-reload
// re-reads exactly this file.
ConfigFile string `yaml:"config_file"`
// KeaDHCP4 is the program name (looked up in PATH) or an absolute path.
KeaDHCP4 string `yaml:"kea_dhcp4"`
// ControlSocket is the UNIX control socket of kea-dhcp4. It must equal
// the control socket of the server's generator settings.
ControlSocket string `yaml:"control_socket"`
// LeasePageSize is the limit of one lease4-get-page command.
LeasePageSize int `yaml:"lease_page_size"`
// CommandTimeout bounds every program run and every control command.
CommandTimeout time.Duration `yaml:"command_timeout"`
}
// DefaultConfig returns the defaults of ADR 0021.
func DefaultConfig() Config {
return Config{
ConfigFile: "/etc/kea/kea-dhcp4.conf",
KeaDHCP4: "kea-dhcp4",
ControlSocket: "/run/kea/kea4-ctrl-socket",
LeasePageSize: 1000,
CommandTimeout: 60 * time.Second,
}
}
// maxSocketPath is the usable length of sun_path on Linux (108 bytes
// including the terminating NUL).
const maxSocketPath = 107
// Validate checks the configuration.
func (c *Config) Validate() error {
var errs []error
if err := checkAbsPath(c.ConfigFile); err != nil {
errs = append(errs, fmt.Errorf("dhcp.config_file: %w", err))
}
if err := checkAbsPath(c.ControlSocket); err != nil {
errs = append(errs, fmt.Errorf("dhcp.control_socket: %w", err))
} else if len(c.ControlSocket) > maxSocketPath {
errs = append(errs, fmt.Errorf("dhcp.control_socket: longer than %d bytes", maxSocketPath))
}
if strings.TrimSpace(c.KeaDHCP4) == "" || strings.ContainsAny(c.KeaDHCP4, "\x00\n") {
errs = append(errs, errors.New("dhcp.kea_dhcp4: program name is empty or invalid"))
}
if c.LeasePageSize < 1 || c.LeasePageSize > 100_000 {
errs = append(errs, fmt.Errorf("dhcp.lease_page_size: %d is out of range [1, 100000]", c.LeasePageSize))
}
if c.CommandTimeout <= 0 || c.CommandTimeout > time.Hour {
errs = append(errs, fmt.Errorf("dhcp.command_timeout: %s is out of range (0, 1h]", c.CommandTimeout))
}
return errors.Join(errs...)
}
func checkAbsPath(p string) error {
if p == "" || !path.IsAbs(p) || path.Clean(p) != p || p == "/" {
return fmt.Errorf("%q must be a clean absolute path", p)
}
if strings.ContainsFunc(p, func(r rune) bool { return r < 0x20 || r == 0x7f }) {
return fmt.Errorf("%q contains a control character", p)
}
return nil
}