a73x

internal/agent/vfkit/leases_test.go

Ref:   Size: 5.7 KiB   History

package vfkit

import (
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/a73x/eitri/internal/agent/state"
)

// lease renders one dhcpd_leases stanza in macOS's format: bootpd writes octets
// unpadded, prefixes the ARP hardware type, and stamps the expiry epoch as hex.
func lease(name, ip, mac string, expiry uint64) string {
	return "{\n\tname=" + name + "\n\tip_address=" + ip + "\n\thw_address=1," + mac +
		"\n\tidentifier=1," + mac + "\n\tlease=0x" + strconv.FormatUint(expiry, 16) + "\n}\n"
}

// Two expiries a day apart, so which one a test means is never in doubt.
const (
	older = 0x68a1b2c3
	newer = 0x68a30443
)

func TestLeaseAddressFindsTheGuestByItsMAC(t *testing.T) {
	// state.MAC pads every octet; bootpd does not. Matching the two as written
	// would miss every lease this backend ever needs to read.
	const padded = "52:54:00:0a:0b:0c"
	leases := lease("other", "192.168.64.2", "52:54:0:ff:ee:dd", newer) +
		lease("web", "192.168.64.7", "52:54:0:a:b:c", newer)

	assert.Equal(t, "192.168.64.7", leaseAddress(leases, padded))
}

func TestLeaseAddressIgnoresALeaseKeyedByADUID(t *testing.T) {
	const mac = "52:54:00:0a:0b:0c"
	// A guest that identifies itself by a DUID gets its lease filed under one:
	// hardware type "ff" and an opaque identifier that is not six octets. Such
	// a stanza is not a candidate for any VM, so it can neither be returned nor
	// displace a real match — which is the whole reason the one lease database
	// captured on real hardware had two stanzas for one guest and still gave
	// one unambiguous answer. The seed stops the guest asking that way at all;
	// this is the parser's half of the same guarantee.
	duid := "{\n\tname=web\n\tip_address=192.168.64.8" +
		"\n\thw_address=ff,f1:f5:dd:7f:0:2:0:0:ab:11:69:49:e9:aa:ed:20:59:eb" +
		"\n\tidentifier=ff,f1:f5:dd:7f:0:2:0:0:ab:11:69:49:e9:aa:ed:20:59:eb\n\tlease=0x6a6e5628\n}\n"
	mine := lease("web", "192.168.64.7", "52:54:0:a:b:c", newer)

	assert.Equal(t, "192.168.64.7", leaseAddress(duid+mine, mac))
	assert.Equal(t, "192.168.64.7", leaseAddress(mine+duid, mac))
	assert.Empty(t, leaseAddress(duid, mac), "a DUID lease belongs to no VM this backend knows")
}

func TestLeaseAddressTakesTheLastOfTwoCandidates(t *testing.T) {
	const mac = "52:54:00:0a:0b:0c"
	// Two stanzas keyed on one MAC should not happen — the guest asks under one
	// address and the seed pins its client identifier to it. Nothing in the file
	// ranks them if they do (`lease` is an expiry, not a grant time), so the
	// rule is arbitrary and pinned here only so that changing it is deliberate.
	leases := lease("web", "192.168.64.7", "52:54:0:a:b:c", older) +
		lease("web", "192.168.64.9", "52:54:0:a:b:c", newer)

	assert.Equal(t, "192.168.64.9", leaseAddress(leases, mac))
}

func TestLeaseAddressIsEmptyWhenTheGuestHasNotAskedYet(t *testing.T) {
	leases := lease("other", "192.168.64.2", "52:54:0:ff:ee:dd", newer)

	// Empty means "no answer", never "no address" — reconcile leaves any
	// recorded address alone rather than clearing it.
	assert.Empty(t, leaseAddress(leases, "52:54:00:0a:0b:0c"))
}

func TestLeaseAddressIgnoresAStanzaWithNoAddress(t *testing.T) {
	leases := "{\n\tname=web\n\thw_address=1,52:54:0:a:b:c\n}\n"

	assert.Empty(t, leaseAddress(leases, "52:54:00:0a:0b:0c"))
}

func TestLeaseAddressSurvivesAMalformedFile(t *testing.T) {
	for name, leases := range map[string]string{
		"empty":            "",
		"no braces":        "ip_address=192.168.64.7\nhw_address=1,52:54:0:a:b:c\n",
		"unterminated":     "{\n\tip_address=192.168.64.7\n\thw_address=1,52:54:0:a:b:c\n",
		"junk hw_address":  lease("web", "192.168.64.7", "not-a-mac", newer),
		"truncated octets": lease("web", "192.168.64.7", "52:54:0:a:b", newer),
		// hw_address is checked exhaustively; ip_address must be too, or a
		// stanza carrying our MAC and junk becomes the address we dial.
		"junk ip_address":  lease("web", "not-an-address", "52:54:0:a:b:c", newer),
		"empty ip_address": lease("web", "", "52:54:0:a:b:c", newer),
		"ip with a port":   lease("web", "192.168.64.7:22", "52:54:0:a:b:c", newer),
	} {
		t.Run(name, func(t *testing.T) {
			// The lease database is written by a system daemon we do not
			// control; nothing in it may make the agent report a wrong address.
			assert.Empty(t, leaseAddress(leases, "52:54:00:0a:0b:0c"))
		})
	}
}

func TestNormalizeMACRejectsAnythingThatIsNotSixOctets(t *testing.T) {
	for _, in := range []string{"", "52:54:00", "52:54:00:0a:0b:0c:0d", "52-54-00-0a-0b-0c", "1,", "gg:54:00:0a:0b:0c"} {
		assert.Empty(t, normalizeMAC(in), "%q must not normalise to anything a real MAC could equal", in)
	}
	// An octet past a byte is not a MAC either, however hex-looking it is.
	assert.Empty(t, normalizeMAC("152:54:00:0a:0b:0c"))
}

func TestAddressReadsTheHostLeaseDatabase(t *testing.T) {
	p := newTestProv(t, nil)
	require.NoError(t, os.WriteFile(p.leasesPath,
		[]byte(lease("web", "192.168.64.7", trimMACZeros(state.MAC("vm-1")), newer)), 0o600))

	assert.Equal(t, "192.168.64.7", p.Address("vm-1"))
}

func TestAddressIsEmptyBeforeAnyGuestHasLeased(t *testing.T) {
	p := newTestProv(t, nil)
	p.leasesPath = filepath.Join(t.TempDir(), "never-written")

	// A Mac that has never run a guest has no lease database at all; that is
	// "no answer", not a failure worth reporting up the seam.
	assert.Empty(t, p.Address("vm-1"))
}

// trimMACZeros rewrites a padded MAC the way bootpd would write it, so the test
// fixture is the file's format rather than ours.
func trimMACZeros(mac string) string {
	octets := strings.Split(mac, ":")
	for i, o := range octets {
		octets[i] = strings.TrimLeft(o, "0")
		if octets[i] == "" {
			octets[i] = "0"
		}
	}
	return strings.Join(octets, ":")
}