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
104
internal/client/dhcp/client.go
Normal file
104
internal/client/dhcp/client.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
package dhcpclient
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
dhcpapi "git.g3e.fr/syonad/two/internal/api/dhcp"
|
||||
)
|
||||
|
||||
const DefaultTimeout = 5 * time.Second
|
||||
|
||||
var ErrNotServed = errors.New("mac is not served by this subnet")
|
||||
|
||||
type Client struct {
|
||||
path string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func New(path string) *Client {
|
||||
return &Client{path: path, timeout: DefaultTimeout}
|
||||
}
|
||||
|
||||
func (c *Client) WithTimeout(d time.Duration) *Client {
|
||||
return &Client{path: c.path, timeout: d}
|
||||
}
|
||||
|
||||
func (c *Client) call(req dhcpapi.Request) (dhcpapi.Response, error) {
|
||||
conn, err := net.DialTimeout("unix", c.path, c.timeout)
|
||||
if err != nil {
|
||||
return dhcpapi.Response{}, fmt.Errorf("dial %s: %w", c.path, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err := conn.SetDeadline(time.Now().Add(c.timeout)); err != nil {
|
||||
return dhcpapi.Response{}, fmt.Errorf("set deadline on %s: %w", c.path, err)
|
||||
}
|
||||
|
||||
raw, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return dhcpapi.Response{}, fmt.Errorf("encode %s: %w", req.Verb, err)
|
||||
}
|
||||
if _, err := conn.Write(append(raw, '\n')); err != nil {
|
||||
return dhcpapi.Response{}, fmt.Errorf("send %s: %w", req.Verb, err)
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(conn)
|
||||
scanner.Buffer(make([]byte, 0, 4096), dhcpapi.MaxMessageBytes)
|
||||
if !scanner.Scan() {
|
||||
if err := scanner.Err(); err != nil {
|
||||
return dhcpapi.Response{}, fmt.Errorf("read reply to %s: %w", req.Verb, err)
|
||||
}
|
||||
return dhcpapi.Response{}, fmt.Errorf("no reply to %s", req.Verb)
|
||||
}
|
||||
|
||||
var resp dhcpapi.Response
|
||||
if err := json.Unmarshal(scanner.Bytes(), &resp); err != nil {
|
||||
return dhcpapi.Response{}, fmt.Errorf("parse reply to %s: %w", req.Verb, err)
|
||||
}
|
||||
if !resp.OK {
|
||||
return resp, fmt.Errorf("%s refused: %s", req.Verb, resp.Error)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) SetSubnet(subnet dhcpapi.Subnet) error {
|
||||
_, err := c.call(dhcpapi.Request{Verb: dhcpapi.VerbSetSubnet, Subnet: &subnet})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) SetHost(host dhcpapi.Host) error {
|
||||
_, err := c.call(dhcpapi.Request{Verb: dhcpapi.VerbSetHost, Host: &host})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) DelHost(mac string) error {
|
||||
_, err := c.call(dhcpapi.Request{Verb: dhcpapi.VerbDelHost, MAC: mac})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) GetState() (dhcpapi.State, string, error) {
|
||||
resp, err := c.call(dhcpapi.Request{Verb: dhcpapi.VerbGetState})
|
||||
if err != nil {
|
||||
return dhcpapi.State{}, "", err
|
||||
}
|
||||
if resp.State == nil {
|
||||
return dhcpapi.State{}, "", errors.New("get-state returned no state")
|
||||
}
|
||||
return *resp.State, resp.Digest, nil
|
||||
}
|
||||
|
||||
func (c *Client) Probe(mac string) (dhcpapi.Lease, error) {
|
||||
resp, err := c.call(dhcpapi.Request{Verb: dhcpapi.VerbProbe, MAC: mac})
|
||||
if err != nil {
|
||||
return dhcpapi.Lease{}, err
|
||||
}
|
||||
if !resp.Served || resp.Lease == nil {
|
||||
return dhcpapi.Lease{}, ErrNotServed
|
||||
}
|
||||
return *resp.Lease, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue