Subnets served per interface, pools, reservations, options at three levels, leases in SQLite saved before the ACK, ping and decline conflict checks, validated changes with history and undo, users and tokens, an audit log, a Kea importer, a sandboxed systemd unit and Debian packages. Tested with real udhcpc and dhcpcd clients in Docker (scripts/e2e.sh). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
179 lines
5.5 KiB
Go
179 lines
5.5 KiB
Go
package dhcp4
|
|
|
|
import (
|
|
"bytes"
|
|
"net"
|
|
"net/netip"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRoundTrip(t *testing.T) {
|
|
mac, _ := net.ParseMAC("aa:bb:cc:00:11:22")
|
|
p := NewRequest(Discover, mac, 0x12345678)
|
|
p.Flags = FlagBroadcast
|
|
p.Set(OptHostName, []byte("laptop"))
|
|
p.Set(OptParamRequest, []byte{1, 3, 6, 15})
|
|
p.Set(OptClientID, append([]byte{1}, mac...))
|
|
b := p.Marshal()
|
|
if len(b) < MinLen {
|
|
t.Fatalf("message is %d bytes, under %d", len(b), MinLen)
|
|
}
|
|
if p.Len() != len(b) {
|
|
t.Errorf("Len() = %d, Marshal gave %d", p.Len(), len(b))
|
|
}
|
|
q, err := Parse(b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if q.Type() != Discover || q.XID != 0x12345678 || !q.Broadcast() || q.MAC().String() != mac.String() {
|
|
t.Errorf("parsed %+v", q)
|
|
}
|
|
if q.HostName() != "laptop" || q.ClientID() != "01aabbcc001122" {
|
|
t.Errorf("host %q client id %q", q.HostName(), q.ClientID())
|
|
}
|
|
if !bytes.Equal(q.ParamRequest(), []byte{1, 3, 6, 15}) {
|
|
t.Errorf("PRL %v", q.ParamRequest())
|
|
}
|
|
r := q.Reply(Offer)
|
|
r.YIAddr = netip.MustParseAddr("10.0.10.57")
|
|
if r.Op != BootReply || r.XID != q.XID || r.Type() != Offer || r.CHAddr != q.CHAddr {
|
|
t.Errorf("reply %+v", r)
|
|
}
|
|
r2, err := Parse(r.Marshal())
|
|
if err != nil || r2.YIAddr != r.YIAddr {
|
|
t.Fatalf("reply round trip: %v %v", r2, err)
|
|
}
|
|
}
|
|
|
|
func TestLongOptionsAreSplitAndJoined(t *testing.T) {
|
|
p := NewRequest(Inform, net.HardwareAddr{1, 2, 3, 4, 5, 6}, 1)
|
|
long := bytes.Repeat([]byte{7}, 600)
|
|
p.Set(43, long)
|
|
b := p.Marshal()
|
|
q, err := Parse(b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(q.Get(43), long) {
|
|
t.Errorf("got %d bytes back, want 600", len(q.Get(43)))
|
|
}
|
|
if p.Len() != len(b) {
|
|
t.Errorf("Len() = %d, Marshal gave %d", p.Len(), len(b))
|
|
}
|
|
}
|
|
|
|
func TestOverload(t *testing.T) {
|
|
p := NewRequest(Request, net.HardwareAddr{1, 2, 3, 4, 5, 6}, 1)
|
|
p.Set(OptOverload, []byte{1})
|
|
b := p.Marshal()
|
|
// Put option 12 in the file field (bytes 108..236).
|
|
copy(b[108:], []byte{12, 3, 'p', 'c', '1', 255})
|
|
q, err := Parse(b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if q.HostName() != "pc1" || q.File != "" {
|
|
t.Errorf("host %q file %q", q.HostName(), q.File)
|
|
}
|
|
}
|
|
|
|
func TestParseErrors(t *testing.T) {
|
|
if _, err := Parse(make([]byte, 100)); err == nil {
|
|
t.Error("short message accepted")
|
|
}
|
|
b := NewRequest(Discover, net.HardwareAddr{1, 2, 3, 4, 5, 6}, 1).Marshal()
|
|
b[236] = 0
|
|
if _, err := Parse(b); err == nil || !strings.Contains(err.Error(), "cookie") {
|
|
t.Errorf("missing cookie: %v", err)
|
|
}
|
|
b = NewRequest(Discover, net.HardwareAddr{1, 2, 3, 4, 5, 6}, 1).Marshal()
|
|
b[240+1] = 200 // message type option claims 200 bytes
|
|
if _, err := Parse(b); err == nil {
|
|
t.Error("overlong option accepted")
|
|
}
|
|
}
|
|
|
|
func TestHostNameFromFQDN(t *testing.T) {
|
|
p := NewRequest(Request, net.HardwareAddr{1, 2, 3, 4, 5, 6}, 1)
|
|
wire, _ := encodeNames([]string{"desk.example.com"})
|
|
p.Set(OptClientFQDN, append([]byte{0x04, 0, 0}, wire...))
|
|
if got := p.HostName(); got != "desk" {
|
|
t.Errorf("wire FQDN: %q", got)
|
|
}
|
|
p.Set(OptClientFQDN, append([]byte{0, 0, 0}, "tv.lan"...))
|
|
if got := p.HostName(); got != "tv" {
|
|
t.Errorf("ASCII FQDN: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestEncodeDecode(t *testing.T) {
|
|
cases := []struct {
|
|
typ, in, out string
|
|
hex string
|
|
}{
|
|
{TypeIP, " 10.0.0.1 ", "10.0.0.1", "0a000001"},
|
|
{TypeIPs, "10.0.0.1,10.0.0.2", "10.0.0.1, 10.0.0.2", "0a0000010a000002"},
|
|
{TypeU8, "8", "8", "08"},
|
|
{TypeU16, "1500", "1500", "05dc"},
|
|
{TypeU32, "86400", "86400", "00015180"},
|
|
{TypeI32, "-3600", "-3600", "fffff1f0"},
|
|
{TypeBool, "yes", "true", "01"},
|
|
{TypeHex, "01:02:0A", "01:02:0a", "01020a"},
|
|
{TypeHex, "0x0102", "01:02", "0102"},
|
|
{TypeDomains, "lan, Example.com.", "lan, example.com", "036c616e00076578616d706c6503636f6d00"},
|
|
{TypeRoutes, "10.20.0.0/16 via 10.0.10.1, 0.0.0.0/0 - 10.0.10.1", "10.20.0.0/16 via 10.0.10.1, 0.0.0.0/0 via 10.0.10.1", "100a140a000a01000a000a01"},
|
|
}
|
|
for _, c := range cases {
|
|
b, err := Encode(c.typ, c.in)
|
|
if err != nil {
|
|
t.Errorf("%s %q: %v", c.typ, c.in, err)
|
|
continue
|
|
}
|
|
if got := Decode(c.typ, b); got != c.out {
|
|
t.Errorf("%s %q decodes as %q, want %q", c.typ, c.in, got, c.out)
|
|
}
|
|
if got := Decode(TypeHex, b); strings.ReplaceAll(got, ":", "") != c.hex {
|
|
t.Errorf("%s %q encodes as %s, want %s", c.typ, c.in, got, c.hex)
|
|
}
|
|
}
|
|
bad := []struct{ typ, in string }{
|
|
{TypeIP, "10.0.0.256"}, {TypeIP, "::1"}, {TypeIPs, ""}, {TypeU8, "256"}, {TypeU16, "-1"},
|
|
{TypeBool, "maybe"}, {TypeHex, "zz"}, {TypeDomains, "a..b"}, {TypeDomains, "bad!name"},
|
|
{TypeRoutes, "10.20.0.0/16"}, {TypeRoutes, "10.20.0.1/16 via 10.0.0.1"}, {TypeString, ""}, {"nope", "x"},
|
|
}
|
|
for _, c := range bad {
|
|
if _, err := Encode(c.typ, c.in); err == nil {
|
|
t.Errorf("%s %q accepted", c.typ, c.in)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDecodeCompressedNames(t *testing.T) {
|
|
// "lan" then "example.lan" written with a pointer to the first "lan".
|
|
b := []byte{3, 'l', 'a', 'n', 0, 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 0xc0, 0}
|
|
if got := Decode(TypeDomains, b); got != "lan, example.lan" {
|
|
t.Errorf("got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestLookup(t *testing.T) {
|
|
if d, ok := Lookup("routers"); !ok || d.Code != 3 {
|
|
t.Errorf("routers: %v %v", d, ok)
|
|
}
|
|
if d, ok := Lookup("42"); !ok || d.Name != "ntp-servers" {
|
|
t.Errorf("42: %v %v", d, ok)
|
|
}
|
|
if d, ok := Lookup("224"); !ok || d.Type != TypeHex || d.Name != "option-224" {
|
|
t.Errorf("224: %v %v", d, ok)
|
|
}
|
|
if _, ok := Lookup("no-such-option"); ok {
|
|
t.Error("unknown name found")
|
|
}
|
|
for _, d := range Catalog {
|
|
if _, ok := Reserved[d.Code]; ok {
|
|
t.Errorf("%s (%d) is in the catalog and reserved", d.Name, d.Code)
|
|
}
|
|
}
|
|
}
|