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) } } }