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>
204 lines
5.2 KiB
Go
204 lines
5.2 KiB
Go
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())
|
|
}
|
|
}
|
|
}
|