f-46: dhcpapi: add the control socket, its protocol and its client #46
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>
This commit is contained in:
parent
8802de4fb0
commit
0c0b58820c
13 changed files with 1482 additions and 445 deletions
83
internal/api/dhcp/convert.go
Normal file
83
internal/api/dhcp/convert.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
package dhcpapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"git.g3e.fr/syonad/two/internal/dhcpd"
|
||||
)
|
||||
|
||||
func (s Subnet) toConfig() (dhcpd.SubnetConfig, error) {
|
||||
_, network, err := net.ParseCIDR(s.Network)
|
||||
if err != nil {
|
||||
return dhcpd.SubnetConfig{}, fmt.Errorf("invalid network %q: %w", s.Network, err)
|
||||
}
|
||||
interfaceIP := net.ParseIP(s.InterfaceIP)
|
||||
if interfaceIP == nil {
|
||||
return dhcpd.SubnetConfig{}, fmt.Errorf("invalid interface ip %q", s.InterfaceIP)
|
||||
}
|
||||
|
||||
c := dhcpd.SubnetConfig{Network: network, InterfaceIP: interfaceIP}
|
||||
|
||||
if s.VPCRoute != "" {
|
||||
if _, c.VPCRoute, err = net.ParseCIDR(s.VPCRoute); err != nil {
|
||||
return dhcpd.SubnetConfig{}, fmt.Errorf("invalid vpc route %q: %w", s.VPCRoute, err)
|
||||
}
|
||||
}
|
||||
if s.DefaultGateway != "" {
|
||||
if c.DefaultGateway = net.ParseIP(s.DefaultGateway); c.DefaultGateway == nil {
|
||||
return dhcpd.SubnetConfig{}, fmt.Errorf("invalid default gateway %q", s.DefaultGateway)
|
||||
}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (h Host) toHost() (dhcpd.Host, error) {
|
||||
mac, err := net.ParseMAC(h.MAC)
|
||||
if err != nil {
|
||||
return dhcpd.Host{}, fmt.Errorf("invalid mac %q: %w", h.MAC, err)
|
||||
}
|
||||
ip := net.ParseIP(h.IP)
|
||||
if ip == nil {
|
||||
return dhcpd.Host{}, fmt.Errorf("invalid host ip %q", h.IP)
|
||||
}
|
||||
return dhcpd.Host{MAC: mac, IP: ip, VM: h.VM, DefaultRoute: h.DefaultRoute}, nil
|
||||
}
|
||||
|
||||
func subnetFromConfig(c dhcpd.SubnetConfig) Subnet {
|
||||
s := Subnet{
|
||||
Network: c.Network.String(),
|
||||
InterfaceIP: c.InterfaceIP.String(),
|
||||
}
|
||||
if c.VPCRoute != nil {
|
||||
s.VPCRoute = c.VPCRoute.String()
|
||||
}
|
||||
if c.DefaultGateway != nil {
|
||||
s.DefaultGateway = c.DefaultGateway.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func hostFromHost(h dhcpd.Host) Host {
|
||||
return Host{
|
||||
MAC: h.MAC.String(),
|
||||
IP: h.IP.String(),
|
||||
VM: h.VM,
|
||||
DefaultRoute: h.DefaultRoute,
|
||||
}
|
||||
}
|
||||
|
||||
func stateFromStore(store *dhcpd.Store) State {
|
||||
state := State{Hosts: make([]Host, 0)}
|
||||
|
||||
if config, configured := store.Subnet(); configured {
|
||||
subnet := subnetFromConfig(config)
|
||||
state.Subnet = &subnet
|
||||
}
|
||||
for _, h := range store.Hosts() {
|
||||
state.Hosts = append(state.Hosts, hostFromHost(h))
|
||||
}
|
||||
SortHosts(state.Hosts)
|
||||
|
||||
return state
|
||||
}
|
||||
116
internal/api/dhcp/digest.go
Normal file
116
internal/api/dhcp/digest.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
package dhcpapi
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func canonicalMAC(s string) (string, error) {
|
||||
mac, err := net.ParseMAC(s)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid mac %q: %w", s, err)
|
||||
}
|
||||
return mac.String(), nil
|
||||
}
|
||||
|
||||
func canonicalIP(s string) (string, error) {
|
||||
ip := net.ParseIP(s)
|
||||
if ip == nil {
|
||||
return "", fmt.Errorf("invalid ip %q", s)
|
||||
}
|
||||
return ip.String(), nil
|
||||
}
|
||||
|
||||
func canonicalCIDR(s string) (string, error) {
|
||||
_, network, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid cidr %q: %w", s, err)
|
||||
}
|
||||
return network.String(), nil
|
||||
}
|
||||
|
||||
func SortHosts(hosts []Host) {
|
||||
sort.Slice(hosts, func(i, j int) bool { return hosts[i].MAC < hosts[j].MAC })
|
||||
}
|
||||
|
||||
func CanonicalSubnet(s Subnet) (Subnet, error) {
|
||||
network, err := canonicalCIDR(s.Network)
|
||||
if err != nil {
|
||||
return Subnet{}, err
|
||||
}
|
||||
interfaceIP, err := canonicalIP(s.InterfaceIP)
|
||||
if err != nil {
|
||||
return Subnet{}, err
|
||||
}
|
||||
|
||||
out := Subnet{Network: network, InterfaceIP: interfaceIP}
|
||||
|
||||
if s.VPCRoute != "" {
|
||||
if out.VPCRoute, err = canonicalCIDR(s.VPCRoute); err != nil {
|
||||
return Subnet{}, err
|
||||
}
|
||||
}
|
||||
if s.DefaultGateway != "" {
|
||||
if out.DefaultGateway, err = canonicalIP(s.DefaultGateway); err != nil {
|
||||
return Subnet{}, err
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func CanonicalHost(h Host) (Host, error) {
|
||||
mac, err := canonicalMAC(h.MAC)
|
||||
if err != nil {
|
||||
return Host{}, err
|
||||
}
|
||||
ip, err := canonicalIP(h.IP)
|
||||
if err != nil {
|
||||
return Host{}, err
|
||||
}
|
||||
return Host{MAC: mac, IP: ip, VM: h.VM, DefaultRoute: h.DefaultRoute}, nil
|
||||
}
|
||||
|
||||
func Canonical(s State) (State, error) {
|
||||
out := State{Hosts: make([]Host, 0, len(s.Hosts))}
|
||||
|
||||
if s.Subnet != nil {
|
||||
subnet, err := CanonicalSubnet(*s.Subnet)
|
||||
if err != nil {
|
||||
return State{}, err
|
||||
}
|
||||
out.Subnet = &subnet
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(s.Hosts))
|
||||
for _, h := range s.Hosts {
|
||||
host, err := CanonicalHost(h)
|
||||
if err != nil {
|
||||
return State{}, err
|
||||
}
|
||||
if _, dup := seen[host.MAC]; dup {
|
||||
return State{}, fmt.Errorf("duplicate mac %s", host.MAC)
|
||||
}
|
||||
seen[host.MAC] = struct{}{}
|
||||
out.Hosts = append(out.Hosts, host)
|
||||
}
|
||||
SortHosts(out.Hosts)
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func Digest(s State) (string, error) {
|
||||
canonical, err := Canonical(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
raw, err := json.Marshal(canonical)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("encode state: %w", err)
|
||||
}
|
||||
sum := sha256.Sum256(raw)
|
||||
return hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
173
internal/api/dhcp/digest_test.go
Normal file
173
internal/api/dhcp/digest_test.go
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
package dhcpapi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func subnet() Subnet {
|
||||
return Subnet{
|
||||
Network: "10.0.5.0/24",
|
||||
InterfaceIP: "10.0.5.1",
|
||||
VPCRoute: "10.0.0.0/16",
|
||||
DefaultGateway: "10.0.5.254",
|
||||
}
|
||||
}
|
||||
|
||||
func hosts() []Host {
|
||||
return []Host{
|
||||
{MAC: "00:22:33:00:00:0a", IP: "10.0.5.10", VM: "vm-a", DefaultRoute: true},
|
||||
{MAC: "00:22:33:00:00:0b", IP: "10.0.5.11", VM: "vm-b"},
|
||||
}
|
||||
}
|
||||
|
||||
func digestOf(t *testing.T, s State) string {
|
||||
t.Helper()
|
||||
d, err := Digest(s)
|
||||
if err != nil {
|
||||
t.Fatalf("Digest: %v", err)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func TestDigest_IsStableAcrossHostOrder(t *testing.T) {
|
||||
sub := subnet()
|
||||
a := State{Subnet: &sub, Hosts: hosts()}
|
||||
|
||||
reversed := hosts()
|
||||
reversed[0], reversed[1] = reversed[1], reversed[0]
|
||||
b := State{Subnet: &sub, Hosts: reversed}
|
||||
|
||||
if digestOf(t, a) != digestOf(t, b) {
|
||||
t.Error("host order must not change the digest: the watchdog would report a phantom drift")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDigest_IsStableAcrossMACCase(t *testing.T) {
|
||||
sub := subnet()
|
||||
a := State{Subnet: &sub, Hosts: hosts()}
|
||||
|
||||
upper := hosts()
|
||||
upper[0].MAC = "00:22:33:00:00:0A"
|
||||
b := State{Subnet: &sub, Hosts: upper}
|
||||
|
||||
if digestOf(t, a) != digestOf(t, b) {
|
||||
t.Error("mac case must not change the digest")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDigest_IsStableAcrossIPv4InIPv6Notation(t *testing.T) {
|
||||
sub := subnet()
|
||||
a := State{Subnet: &sub, Hosts: hosts()}
|
||||
|
||||
mapped := subnet()
|
||||
mapped.InterfaceIP = "::ffff:10.0.5.1"
|
||||
b := State{Subnet: &mapped, Hosts: hosts()}
|
||||
|
||||
if digestOf(t, a) != digestOf(t, b) {
|
||||
t.Error("the same address written in ipv4-mapped form must hash alike")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDigest_ChangesWhenAHostIPChanges(t *testing.T) {
|
||||
sub := subnet()
|
||||
a := State{Subnet: &sub, Hosts: hosts()}
|
||||
|
||||
moved := hosts()
|
||||
moved[0].IP = "10.0.5.99"
|
||||
b := State{Subnet: &sub, Hosts: moved}
|
||||
|
||||
if digestOf(t, a) == digestOf(t, b) {
|
||||
t.Error("a changed reservation must change the digest")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDigest_ChangesWhenTheDefaultRouteFlagChanges(t *testing.T) {
|
||||
sub := subnet()
|
||||
a := State{Subnet: &sub, Hosts: hosts()}
|
||||
|
||||
flipped := hosts()
|
||||
flipped[0].DefaultRoute = false
|
||||
b := State{Subnet: &sub, Hosts: flipped}
|
||||
|
||||
if digestOf(t, a) == digestOf(t, b) {
|
||||
t.Error("the default route flag is part of the served state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDigest_ChangesWhenTheSubnetChanges(t *testing.T) {
|
||||
sub := subnet()
|
||||
a := State{Subnet: &sub, Hosts: hosts()}
|
||||
|
||||
other := subnet()
|
||||
other.DefaultGateway = "10.0.5.253"
|
||||
b := State{Subnet: &other, Hosts: hosts()}
|
||||
|
||||
if digestOf(t, a) == digestOf(t, b) {
|
||||
t.Error("the subnet configuration is part of the served state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDigest_DistinguishesNoSubnetFromAConfiguredOne(t *testing.T) {
|
||||
sub := subnet()
|
||||
configured := State{Subnet: &sub}
|
||||
bare := State{}
|
||||
|
||||
if digestOf(t, configured) == digestOf(t, bare) {
|
||||
t.Error("an unconfigured subnet must not hash like a configured one")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDigest_EmptyAndNilHostsHashAlike(t *testing.T) {
|
||||
if digestOf(t, State{Hosts: nil}) != digestOf(t, State{Hosts: []Host{}}) {
|
||||
t.Error("nil and empty host lists describe the same state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDigest_RejectsAnInvalidMAC(t *testing.T) {
|
||||
if _, err := Digest(State{Hosts: []Host{{MAC: "nope", IP: "10.0.5.10"}}}); err == nil {
|
||||
t.Fatal("an invalid mac must be reported, not hashed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDigest_RejectsAnInvalidIP(t *testing.T) {
|
||||
if _, err := Digest(State{Hosts: []Host{{MAC: "00:22:33:00:00:0a", IP: "10.0.5.300"}}}); err == nil {
|
||||
t.Fatal("an invalid ip must be reported, not hashed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonical_RejectsADuplicateMAC(t *testing.T) {
|
||||
dup := []Host{
|
||||
{MAC: "00:22:33:00:00:0a", IP: "10.0.5.10"},
|
||||
{MAC: "00:22:33:00:00:0A", IP: "10.0.5.11"},
|
||||
}
|
||||
if _, err := Canonical(State{Hosts: dup}); err == nil {
|
||||
t.Fatal("the same mac twice is an inconsistent state, not something to hash")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonical_NormalizesTheNetworkToItsBaseAddress(t *testing.T) {
|
||||
sub := subnet()
|
||||
sub.Network = "10.0.5.42/24"
|
||||
|
||||
got, err := CanonicalSubnet(sub)
|
||||
if err != nil {
|
||||
t.Fatalf("CanonicalSubnet: %v", err)
|
||||
}
|
||||
if got.Network != "10.0.5.0/24" {
|
||||
t.Errorf("network = %s, want 10.0.5.0/24", got.Network)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonical_SortsHostsByMAC(t *testing.T) {
|
||||
unsorted := []Host{
|
||||
{MAC: "00:22:33:00:00:0c", IP: "10.0.5.12"},
|
||||
{MAC: "00:22:33:00:00:0a", IP: "10.0.5.10"},
|
||||
}
|
||||
got, err := Canonical(State{Hosts: unsorted})
|
||||
if err != nil {
|
||||
t.Fatalf("Canonical: %v", err)
|
||||
}
|
||||
if got.Hosts[0].MAC != "00:22:33:00:00:0a" {
|
||||
t.Errorf("hosts = %v, want sorted by mac", got.Hosts)
|
||||
}
|
||||
}
|
||||
58
internal/api/dhcp/models.go
Normal file
58
internal/api/dhcp/models.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package dhcpapi
|
||||
|
||||
type Verb string
|
||||
|
||||
const (
|
||||
VerbSetSubnet Verb = "set-subnet"
|
||||
VerbSetHost Verb = "set-host"
|
||||
VerbDelHost Verb = "del-host"
|
||||
VerbGetState Verb = "get-state"
|
||||
VerbProbe Verb = "probe"
|
||||
)
|
||||
|
||||
const MaxMessageBytes = 64 * 1024
|
||||
|
||||
type Subnet struct {
|
||||
Network string `json:"network"`
|
||||
InterfaceIP string `json:"interface_ip"`
|
||||
VPCRoute string `json:"vpc_route,omitempty"`
|
||||
DefaultGateway string `json:"default_gateway,omitempty"`
|
||||
}
|
||||
|
||||
type Host struct {
|
||||
MAC string `json:"mac"`
|
||||
IP string `json:"ip"`
|
||||
VM string `json:"vm,omitempty"`
|
||||
DefaultRoute bool `json:"default_route"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
Subnet *Subnet `json:"subnet,omitempty"`
|
||||
Hosts []Host `json:"hosts"`
|
||||
}
|
||||
|
||||
type Lease struct {
|
||||
MAC string `json:"mac"`
|
||||
IP string `json:"ip"`
|
||||
Netmask string `json:"netmask"`
|
||||
Router string `json:"router,omitempty"`
|
||||
DNS []string `json:"dns"`
|
||||
Routes []string `json:"routes"`
|
||||
LeaseSeconds uint32 `json:"lease_seconds"`
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Verb Verb `json:"verb"`
|
||||
Subnet *Subnet `json:"subnet,omitempty"`
|
||||
Host *Host `json:"host,omitempty"`
|
||||
MAC string `json:"mac,omitempty"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
OK bool `json:"ok"`
|
||||
Error string `json:"error,omitempty"`
|
||||
State *State `json:"state,omitempty"`
|
||||
Digest string `json:"digest,omitempty"`
|
||||
Lease *Lease `json:"lease,omitempty"`
|
||||
Served bool `json:"served,omitempty"`
|
||||
}
|
||||
190
internal/api/dhcp/server.go
Normal file
190
internal/api/dhcp/server.go
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
package dhcpapi
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.g3e.fr/syonad/two/internal/dhcpd"
|
||||
"git.g3e.fr/syonad/two/pkg/db/statefile"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
)
|
||||
|
||||
const SocketMode = 0o600
|
||||
|
||||
type Server struct {
|
||||
store *dhcpd.Store
|
||||
listener net.Listener
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func Listen(store *dhcpd.Store, path string, logger *slog.Logger) (*Server, error) {
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, statefile.DirMode); err != nil {
|
||||
return nil, fmt.Errorf("create %s: %w", dir, err)
|
||||
}
|
||||
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, fmt.Errorf("remove stale socket %s: %w", path, err)
|
||||
}
|
||||
|
||||
listener, err := net.Listen("unix", path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listen on %s: %w", path, err)
|
||||
}
|
||||
if err := os.Chmod(path, SocketMode); err != nil {
|
||||
listener.Close()
|
||||
return nil, fmt.Errorf("chmod %s: %w", path, err)
|
||||
}
|
||||
|
||||
return &Server{store: store, listener: listener, logger: logger}, nil
|
||||
}
|
||||
|
||||
func (s *Server) Addr() string {
|
||||
return s.listener.Addr().String()
|
||||
}
|
||||
|
||||
func (s *Server) Close() error {
|
||||
return s.listener.Close()
|
||||
}
|
||||
|
||||
func (s *Server) Serve() error {
|
||||
for {
|
||||
conn, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go s.handleConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleConn(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
s.logger.Error("control connection panicked", "panic", r)
|
||||
}
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(conn)
|
||||
scanner.Buffer(make([]byte, 0, 4096), MaxMessageBytes)
|
||||
encoder := json.NewEncoder(conn)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var req Request
|
||||
if err := json.Unmarshal(line, &req); err != nil {
|
||||
if err := encoder.Encode(failure(fmt.Errorf("malformed request: %w", err))); err != nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := encoder.Encode(s.dispatch(req)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
s.logger.Error("control connection read failed", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func failure(err error) Response {
|
||||
return Response{OK: false, Error: err.Error()}
|
||||
}
|
||||
|
||||
func (s *Server) dispatch(req Request) Response {
|
||||
switch req.Verb {
|
||||
case VerbSetSubnet:
|
||||
if req.Subnet == nil {
|
||||
return failure(errors.New("set-subnet requires a subnet"))
|
||||
}
|
||||
config, err := req.Subnet.toConfig()
|
||||
if err != nil {
|
||||
return failure(err)
|
||||
}
|
||||
if err := s.store.SetSubnet(config); err != nil {
|
||||
return failure(err)
|
||||
}
|
||||
return Response{OK: true}
|
||||
|
||||
case VerbSetHost:
|
||||
if req.Host == nil {
|
||||
return failure(errors.New("set-host requires a host"))
|
||||
}
|
||||
host, err := req.Host.toHost()
|
||||
if err != nil {
|
||||
return failure(err)
|
||||
}
|
||||
if err := s.store.SetHost(host); err != nil {
|
||||
return failure(err)
|
||||
}
|
||||
return Response{OK: true}
|
||||
|
||||
case VerbDelHost:
|
||||
mac, err := net.ParseMAC(req.MAC)
|
||||
if err != nil {
|
||||
return failure(fmt.Errorf("invalid mac %q: %w", req.MAC, err))
|
||||
}
|
||||
if err := s.store.DelHost(mac); err != nil {
|
||||
return failure(err)
|
||||
}
|
||||
return Response{OK: true}
|
||||
|
||||
case VerbGetState:
|
||||
state := stateFromStore(s.store)
|
||||
digest, err := Digest(state)
|
||||
if err != nil {
|
||||
return failure(err)
|
||||
}
|
||||
return Response{OK: true, State: &state, Digest: digest}
|
||||
|
||||
case VerbProbe:
|
||||
mac, err := net.ParseMAC(req.MAC)
|
||||
if err != nil {
|
||||
return failure(fmt.Errorf("invalid mac %q: %w", req.MAC, err))
|
||||
}
|
||||
reply, err := s.store.Probe(mac)
|
||||
if err != nil {
|
||||
return failure(err)
|
||||
}
|
||||
if reply == nil {
|
||||
return Response{OK: true, Served: false}
|
||||
}
|
||||
return Response{OK: true, Served: true, Lease: leaseFromReply(mac, reply)}
|
||||
|
||||
default:
|
||||
return failure(fmt.Errorf("unknown verb %q", req.Verb))
|
||||
}
|
||||
}
|
||||
|
||||
func leaseFromReply(mac net.HardwareAddr, reply *dhcpv4.DHCPv4) *Lease {
|
||||
lease := &Lease{
|
||||
MAC: mac.String(),
|
||||
IP: reply.YourIPAddr.String(),
|
||||
Netmask: net.IP(reply.SubnetMask()).String(),
|
||||
DNS: make([]string, 0, 2),
|
||||
Routes: make([]string, 0, 3),
|
||||
LeaseSeconds: uint32(reply.IPAddressLeaseTime(0).Seconds()),
|
||||
}
|
||||
|
||||
if routers := reply.Router(); len(routers) > 0 {
|
||||
lease.Router = routers[0].String()
|
||||
}
|
||||
for _, dns := range reply.DNS() {
|
||||
lease.DNS = append(lease.DNS, dns.String())
|
||||
}
|
||||
for _, route := range reply.ClasslessStaticRoute() {
|
||||
lease.Routes = append(lease.Routes, route.Dest.String()+" via "+route.Router.String())
|
||||
}
|
||||
return lease
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue