f-46: dhcpd: add the decision engine and the persisted state #46
Store porte la configuration du subnet et les réservations par MAC, et les persiste dans un .state écrit de façon atomique (temp + chmod 0600 + rename). Chaque écriture remplace le fichier, donc un chmod externe est effacé. Les snapshots sont à la fois la forme sérialisée et la forme d'entrée des ordres : validation au moment où l'agent pousse, pas quand un guest demande une adresse. Les hôtes sont triés par MAC, base du digest de L3. Handle rend (nil, nil) — le silence est un retour normal — pour un type de message non traité, un subnet non configuré ou une MAC inconnue. La MAC est normalisée à l'écriture comme au chargement. 58 tests, -race propre, dix mutations toutes détectées. Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
parent
8658acfe08
commit
e44f3aca96
4 changed files with 901 additions and 0 deletions
204
internal/dhcpd/engine_test.go
Normal file
204
internal/dhcpd/engine_test.go
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
package dhcpd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
)
|
||||
|
||||
func configuredStore(t *testing.T) *Store {
|
||||
t.Helper()
|
||||
s, _ := loadedStore(t)
|
||||
if err := s.SetSubnet(testSubnetSnapshot()); err != nil {
|
||||
t.Fatalf("SetSubnet: %v", err)
|
||||
}
|
||||
if err := s.SetHost(testHostSnapshot()); err != nil {
|
||||
t.Fatalf("SetHost: %v", err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func mac(t *testing.T, s string) net.HardwareAddr {
|
||||
t.Helper()
|
||||
m, err := net.ParseMAC(s)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseMAC(%q): %v", s, err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func TestHandle_KnownMACGetsAnOfferOnDiscover(t *testing.T) {
|
||||
s := configuredStore(t)
|
||||
|
||||
reply, err := s.Handle(request(t, dhcpv4.MessageTypeDiscover, mac(t, "00:22:33:00:00:0a")))
|
||||
if err != nil {
|
||||
t.Fatalf("Handle: %v", err)
|
||||
}
|
||||
if reply == nil {
|
||||
t.Fatal("a known mac must be answered")
|
||||
}
|
||||
if reply.MessageType() != dhcpv4.MessageTypeOffer {
|
||||
t.Errorf("message type = %s, want OFFER", reply.MessageType())
|
||||
}
|
||||
if !reply.YourIPAddr.Equal(net.ParseIP("10.0.5.10")) {
|
||||
t.Errorf("yiaddr = %s, want the reserved 10.0.5.10", reply.YourIPAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandle_UnknownMACIsAnsweredWithSilence(t *testing.T) {
|
||||
s := configuredStore(t)
|
||||
|
||||
reply, err := s.Handle(request(t, dhcpv4.MessageTypeDiscover, mac(t, "00:22:33:ff:ff:ff")))
|
||||
if err != nil {
|
||||
t.Fatalf("an unknown mac is not an error: %v", err)
|
||||
}
|
||||
if reply != nil {
|
||||
t.Error("an unknown mac must get no reply, not a NAK")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandle_UnconfiguredSubnetIsAnsweredWithSilence(t *testing.T) {
|
||||
s, _ := loadedStore(t)
|
||||
if err := s.SetHost(testHostSnapshot()); err != nil {
|
||||
t.Fatalf("SetHost: %v", err)
|
||||
}
|
||||
|
||||
reply, err := s.Handle(request(t, dhcpv4.MessageTypeDiscover, mac(t, "00:22:33:00:00:0a")))
|
||||
if err != nil {
|
||||
t.Fatalf("Handle: %v", err)
|
||||
}
|
||||
if reply != nil {
|
||||
t.Error("without a subnet configuration the server must stay silent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandle_ReleaseIsANoOp(t *testing.T) {
|
||||
s := configuredStore(t)
|
||||
|
||||
reply, err := s.Handle(request(t, dhcpv4.MessageTypeRelease, mac(t, "00:22:33:00:00:0a")))
|
||||
if err != nil {
|
||||
t.Fatalf("a RELEASE is not an error: %v", err)
|
||||
}
|
||||
if reply != nil {
|
||||
t.Error("a RELEASE must get no reply: reservations are static")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandle_DeclineIsANoOp(t *testing.T) {
|
||||
s := configuredStore(t)
|
||||
|
||||
reply, err := s.Handle(request(t, dhcpv4.MessageTypeDecline, mac(t, "00:22:33:00:00:0a")))
|
||||
if err != nil {
|
||||
t.Fatalf("a DECLINE is not an error: %v", err)
|
||||
}
|
||||
if reply != nil {
|
||||
t.Error("a DECLINE must get no reply: there is nothing to release")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandle_RequestIsAnsweredWithAnAck(t *testing.T) {
|
||||
s := configuredStore(t)
|
||||
|
||||
reply, err := s.Handle(request(t, dhcpv4.MessageTypeRequest, mac(t, "00:22:33:00:00:0a")))
|
||||
if err != nil {
|
||||
t.Fatalf("Handle: %v", err)
|
||||
}
|
||||
if reply == nil || reply.MessageType() != dhcpv4.MessageTypeAck {
|
||||
t.Fatalf("reply = %v, want an ACK", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandle_NilRequestIsRejected(t *testing.T) {
|
||||
s := configuredStore(t)
|
||||
|
||||
if _, err := s.Handle(nil); !errors.Is(err, ErrNoRequest) {
|
||||
t.Fatalf("error = %v, want ErrNoRequest", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandle_DeletedHostStopsBeingAnswered(t *testing.T) {
|
||||
s := configuredStore(t)
|
||||
if err := s.DelHost("00:22:33:00:00:0a"); err != nil {
|
||||
t.Fatalf("DelHost: %v", err)
|
||||
}
|
||||
|
||||
reply, err := s.Handle(request(t, dhcpv4.MessageTypeDiscover, mac(t, "00:22:33:00:00:0a")))
|
||||
if err != nil {
|
||||
t.Fatalf("Handle: %v", err)
|
||||
}
|
||||
if reply != nil {
|
||||
t.Error("a deleted host must no longer be served")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbe_ReturnsWhatWouldBeSentToTheMAC(t *testing.T) {
|
||||
s := configuredStore(t)
|
||||
|
||||
reply, err := s.Probe("00:22:33:00:00:0A")
|
||||
if err != nil {
|
||||
t.Fatalf("Probe: %v", err)
|
||||
}
|
||||
if reply == nil {
|
||||
t.Fatal("a known mac must be described")
|
||||
}
|
||||
if !reply.YourIPAddr.Equal(net.ParseIP("10.0.5.10")) {
|
||||
t.Errorf("yiaddr = %s, want 10.0.5.10", reply.YourIPAddr)
|
||||
}
|
||||
if got := reply.ClasslessStaticRoute(); len(got) != 3 {
|
||||
t.Errorf("routes = %s, want metadata, vpc and default", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbe_UnknownMACReturnsNothing(t *testing.T) {
|
||||
s := configuredStore(t)
|
||||
|
||||
reply, err := s.Probe("00:22:33:ff:ff:ff")
|
||||
if err != nil {
|
||||
t.Fatalf("Probe: %v", err)
|
||||
}
|
||||
if reply != nil {
|
||||
t.Error("an unknown mac must describe no reply")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbe_WithoutSubnetConfigurationIsRejected(t *testing.T) {
|
||||
s, _ := loadedStore(t)
|
||||
|
||||
if _, err := s.Probe("00:22:33:00:00:0a"); !errors.Is(err, ErrNotConfigured) {
|
||||
t.Fatalf("error = %v, want ErrNotConfigured", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbe_InvalidMACIsRejected(t *testing.T) {
|
||||
s := configuredStore(t)
|
||||
|
||||
if _, err := s.Probe("nope"); err == nil {
|
||||
t.Fatal("an invalid mac must be reported")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandle_SecondaryInterfaceGetsNoDefaultRoute(t *testing.T) {
|
||||
s := configuredStore(t)
|
||||
snap := testHostSnapshot()
|
||||
snap.MAC = "00:22:33:00:00:0b"
|
||||
snap.IP = "10.0.5.11"
|
||||
snap.DefaultRoute = false
|
||||
if err := s.SetHost(snap); err != nil {
|
||||
t.Fatalf("SetHost: %v", err)
|
||||
}
|
||||
|
||||
reply, err := s.Handle(request(t, dhcpv4.MessageTypeRequest, mac(t, "00:22:33:00:00:0b")))
|
||||
if err != nil {
|
||||
t.Fatalf("Handle: %v", err)
|
||||
}
|
||||
if got := reply.Router(); len(got) != 0 {
|
||||
t.Errorf("router option = %v, want none on a secondary interface", got)
|
||||
}
|
||||
for _, r := range reply.ClasslessStaticRoute() {
|
||||
if ones, _ := r.Dest.Mask.Size(); ones == 0 {
|
||||
t.Errorf("unexpected default route for a secondary interface: %s", reply.ClasslessStaticRoute())
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue