Le contrat et le listener vont dans internal/api/dhcp (package dhcpapi), sur la forme de internal/api/agent, et le client dans internal/client/dhcp. Chaînage des imports : statefile <- dhcpd <- dhcpapi <- dhcpclient, sans cycle. dhcpd parle net.IP et net.IPNet et garde ses structs disque privées ; dhcpapi parle chaînes JSON et convertit à la frontière. Un même type portait jusqu'ici le format du fil, la signature du Store et le format du .state — ce qui couplait le fichier au protocole alors que le ticket le décrit comme un détail interne. Le digest est calculé sur une forme canonique partagée par les deux côtés : MAC, IP et CIDR normalisés, hôtes triés, doublon de MAC refusé. Un écart de digest signale donc une vraie divergence, pas une différence d'écriture. Le listener pose un recover par connexion, plafonne les lignes à 64 Kio, refuse une ligne malformée sans fermer la connexion, écoute en 0600 et supprime une socket résiduelle avant le bind. Le client pose une deadline. 101 tests au total, -race propre, treize mutations toutes détectées. Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
255 lines
5.1 KiB
Go
255 lines
5.1 KiB
Go
package dhcpd
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"sort"
|
|
"sync"
|
|
|
|
"git.g3e.fr/syonad/two/pkg/db/statefile"
|
|
)
|
|
|
|
var (
|
|
ErrNoMAC = errors.New("host mac is required")
|
|
ErrNotConfigured = errors.New("subnet is not configured")
|
|
)
|
|
|
|
type diskSubnet struct {
|
|
Network string `json:"network"`
|
|
InterfaceIP string `json:"interface_ip"`
|
|
VPCRoute string `json:"vpc_route,omitempty"`
|
|
DefaultGateway string `json:"default_gateway,omitempty"`
|
|
}
|
|
|
|
type diskHost struct {
|
|
MAC string `json:"mac"`
|
|
IP string `json:"ip"`
|
|
VM string `json:"vm,omitempty"`
|
|
DefaultRoute bool `json:"default_route"`
|
|
}
|
|
|
|
type diskState struct {
|
|
Subnet *diskSubnet `json:"subnet,omitempty"`
|
|
Hosts []diskHost `json:"hosts"`
|
|
}
|
|
|
|
type Store struct {
|
|
mu sync.RWMutex
|
|
file *statefile.File[diskState]
|
|
subnet SubnetConfig
|
|
configured bool
|
|
hosts map[string]Host
|
|
}
|
|
|
|
func NewStore(path string) *Store {
|
|
return &Store{
|
|
file: statefile.New[diskState](path),
|
|
hosts: make(map[string]Host),
|
|
}
|
|
}
|
|
|
|
func (s *Store) Path() string {
|
|
return s.file.Path()
|
|
}
|
|
|
|
func (s *Store) Load() error {
|
|
state, err := s.file.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
subnet := SubnetConfig{}
|
|
configured := false
|
|
if state.Subnet != nil {
|
|
parsed, err := subnetFromDisk(*state.Subnet)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
subnet = parsed
|
|
configured = true
|
|
}
|
|
|
|
hosts := make(map[string]Host, len(state.Hosts))
|
|
for _, h := range state.Hosts {
|
|
host, err := hostFromDisk(h)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hosts[host.MAC.String()] = host
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.subnet = subnet
|
|
s.configured = configured
|
|
s.hosts = hosts
|
|
return nil
|
|
}
|
|
|
|
func subnetFromDisk(d diskSubnet) (SubnetConfig, error) {
|
|
_, network, err := net.ParseCIDR(d.Network)
|
|
if err != nil {
|
|
return SubnetConfig{}, fmt.Errorf("invalid network %q: %w", d.Network, err)
|
|
}
|
|
interfaceIP := net.ParseIP(d.InterfaceIP)
|
|
if interfaceIP == nil {
|
|
return SubnetConfig{}, ErrNoInterfaceIP
|
|
}
|
|
|
|
c := SubnetConfig{Network: network, InterfaceIP: interfaceIP}
|
|
|
|
if d.VPCRoute != "" {
|
|
if _, c.VPCRoute, err = net.ParseCIDR(d.VPCRoute); err != nil {
|
|
return SubnetConfig{}, fmt.Errorf("invalid vpc route %q: %w", d.VPCRoute, err)
|
|
}
|
|
}
|
|
if d.DefaultGateway != "" {
|
|
if c.DefaultGateway = net.ParseIP(d.DefaultGateway); c.DefaultGateway == nil {
|
|
return SubnetConfig{}, fmt.Errorf("invalid default gateway %q", d.DefaultGateway)
|
|
}
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func hostFromDisk(d diskHost) (Host, error) {
|
|
mac, err := net.ParseMAC(d.MAC)
|
|
if err != nil {
|
|
return Host{}, fmt.Errorf("invalid mac %q: %w", d.MAC, err)
|
|
}
|
|
ip := net.ParseIP(d.IP)
|
|
if ip == nil {
|
|
return Host{}, fmt.Errorf("invalid host ip %q", d.IP)
|
|
}
|
|
return Host{MAC: mac, IP: ip, VM: d.VM, DefaultRoute: d.DefaultRoute}, nil
|
|
}
|
|
|
|
func (s *Store) SetSubnet(c SubnetConfig) error {
|
|
if c.Network == nil {
|
|
return ErrNoNetwork
|
|
}
|
|
if c.InterfaceIP == nil {
|
|
return ErrNoInterfaceIP
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
previous, wasConfigured := s.subnet, s.configured
|
|
s.subnet, s.configured = c, true
|
|
|
|
if err := s.persist(); err != nil {
|
|
s.subnet, s.configured = previous, wasConfigured
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) SetHost(h Host) error {
|
|
if len(h.MAC) == 0 {
|
|
return ErrNoMAC
|
|
}
|
|
if h.IP == nil {
|
|
return ErrNoHostIP
|
|
}
|
|
|
|
key := h.MAC.String()
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
previous, existed := s.hosts[key]
|
|
s.hosts[key] = h
|
|
|
|
if err := s.persist(); err != nil {
|
|
if existed {
|
|
s.hosts[key] = previous
|
|
} else {
|
|
delete(s.hosts, key)
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) DelHost(mac net.HardwareAddr) error {
|
|
if len(mac) == 0 {
|
|
return ErrNoMAC
|
|
}
|
|
key := mac.String()
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
previous, existed := s.hosts[key]
|
|
delete(s.hosts, key)
|
|
|
|
if err := s.persist(); err != nil {
|
|
if existed {
|
|
s.hosts[key] = previous
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) Lookup(mac net.HardwareAddr) (Host, bool) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
h, ok := s.hosts[mac.String()]
|
|
return h, ok
|
|
}
|
|
|
|
func (s *Store) Subnet() (SubnetConfig, bool) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
return s.subnet, s.configured
|
|
}
|
|
|
|
func (s *Store) Hosts() []Host {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
return s.sortedHosts()
|
|
}
|
|
|
|
func (s *Store) sortedHosts() []Host {
|
|
hosts := make([]Host, 0, len(s.hosts))
|
|
for _, h := range s.hosts {
|
|
hosts = append(hosts, h)
|
|
}
|
|
sort.Slice(hosts, func(i, j int) bool { return hosts[i].MAC.String() < hosts[j].MAC.String() })
|
|
return hosts
|
|
}
|
|
|
|
func (s *Store) persist() error {
|
|
state := diskState{Hosts: make([]diskHost, 0, len(s.hosts))}
|
|
|
|
if s.configured {
|
|
sub := diskSubnet{
|
|
Network: s.subnet.Network.String(),
|
|
InterfaceIP: s.subnet.InterfaceIP.String(),
|
|
}
|
|
if s.subnet.VPCRoute != nil {
|
|
sub.VPCRoute = s.subnet.VPCRoute.String()
|
|
}
|
|
if s.subnet.DefaultGateway != nil {
|
|
sub.DefaultGateway = s.subnet.DefaultGateway.String()
|
|
}
|
|
state.Subnet = &sub
|
|
}
|
|
|
|
for _, h := range s.sortedHosts() {
|
|
state.Hosts = append(state.Hosts, diskHost{
|
|
MAC: h.MAC.String(),
|
|
IP: h.IP.String(),
|
|
VM: h.VM,
|
|
DefaultRoute: h.DefaultRoute,
|
|
})
|
|
}
|
|
|
|
return s.file.Save(state)
|
|
}
|