New Upstream Release - golang-github-insomniacslk-dhcp
Ready changes
Summary
Merged new upstream version: 0.0~git20230516.4980196 (was: 0.0~git20220915.043f172).
Diff
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 2777592..1e39b34 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -7,12 +7,12 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- go: ['1.13', '1.14', '1.15', '1.16']
+ go: ['1.18', '1.19', '1.20']
env:
GO111MODULE: on
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-go@v2
+ - uses: actions/checkout@v3
+ - uses: actions/setup-go@v3
with:
stable: false
go-version: ${{ matrix.go }}
@@ -28,22 +28,17 @@ jobs:
fi
done
- name: report coverage to codecov
- uses: codecov/codecov-action@v1
- with:
- files: coverage.txt
- flags: unittests
- fail_ci_if_error: true
- verbose: true
+ uses: codecov/codecov-action@v3
integration-tests:
runs-on: ubuntu-latest
strategy:
matrix:
- go: ['1.13', '1.14', '1.15', '1.16']
+ go: ['1.18', '1.19', '1.20']
env:
GO111MODULE: on
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-go@v2
+ - uses: actions/checkout@v3
+ - uses: actions/setup-go@v3
with:
stable: false
go-version: ${{ matrix.go }}
@@ -62,9 +57,4 @@ jobs:
fi
done
- name: report coverage to codecov
- uses: codecov/codecov-action@v1
- with:
- files: coverage.txt
- flags: integtests
- fail_ci_if_error: true
- verbose: true
+ uses: codecov/codecov-action@v3
diff --git a/README.md b/README.md
index 979bc7a..741cb84 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# dhcp
-[![Build Status](https://img.shields.io/github/workflow/status/insomniacslk/dhcp/Tests/master)](https://github.com/insomniacslk/dhcp/actions?query=branch%3Amaster)
+[![Build Status](https://img.shields.io/github/actions/workflow/status/insomniacslk/dhcp/tests.yml?branch=master)](https://github.com/insomniacslk/dhcp/actions?query=branch%3Amaster)
[![GoDoc](https://godoc.org/github.com/insomniacslk/dhcp?status.svg)](https://godoc.org/github.com/insomniacslk/dhcp)
[![codecov](https://codecov.io/gh/insomniacslk/dhcp/branch/master/graph/badge.svg)](https://codecov.io/gh/insomniacslk/dhcp)
[![Go Report Card](https://goreportcard.com/badge/github.com/insomniacslk/dhcp)](https://goreportcard.com/report/github.com/insomniacslk/dhcp)
diff --git a/debian/changelog b/debian/changelog
index ffda288..791ccbb 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+golang-github-insomniacslk-dhcp (0.0~git20230516.4980196-1) UNRELEASED; urgency=low
+
+ * New upstream snapshot.
+
+ -- Debian Janitor <janitor@jelmer.uk> Mon, 22 May 2023 16:23:52 -0000
+
golang-github-insomniacslk-dhcp (0.0~git20220915.043f172-2) unstable; urgency=medium
[ Debian Janitor ]
diff --git a/dhcpv4/dhcpv4.go b/dhcpv4/dhcpv4.go
index e2fd190..6043cd9 100644
--- a/dhcpv4/dhcpv4.go
+++ b/dhcpv4/dhcpv4.go
@@ -222,8 +222,7 @@ func PrependModifiers(m []Modifier, other ...Modifier) []Modifier {
// NewInform builds a new DHCPv4 Informational message with the specified
// hardware address.
func NewInform(hwaddr net.HardwareAddr, localIP net.IP, modifiers ...Modifier) (*DHCPv4, error) {
- return New(PrependModifiers(
- modifiers,
+ return New(PrependModifiers(modifiers,
WithHwAddr(hwaddr),
WithMessageType(MessageTypeInform),
WithClientIP(localIP),
@@ -249,18 +248,22 @@ func NewRequestFromOffer(offer *DHCPv4, modifiers ...Modifier) (*DHCPv4, error)
)...)
}
-// NewRenewFromOffer builds a DHCPv4 RENEW-style request from an offer. RENEW requests have minor
-// changes to their options compared to SELECT requests as specified by RFC 2131, section 4.3.2.
-func NewRenewFromOffer(offer *DHCPv4, modifiers ...Modifier) (*DHCPv4, error) {
- return NewRequestFromOffer(offer, PrependModifiers(modifiers,
- // The server identifier option must not be filled in
- WithoutOption(OptionServerIdentifier),
- // The requested IP address must not be filled in
- WithoutOption(OptionRequestedIPAddress),
+// NewRenewFromAck builds a DHCPv4 RENEW-style request from the ACK of a lease. RENEW requests have
+// minor changes to their options compared to SELECT requests as specified by RFC 2131, section 4.3.2.
+func NewRenewFromAck(ack *DHCPv4, modifiers ...Modifier) (*DHCPv4, error) {
+ return New(PrependModifiers(modifiers,
+ WithReply(ack),
+ WithMessageType(MessageTypeRequest),
// The client IP must be filled in with the IP offered to the client
- WithClientIP(offer.YourIPAddr),
+ WithClientIP(ack.YourIPAddr),
// The renewal request must use unicast
WithBroadcast(false),
+ WithRequestedOptions(
+ OptionSubnetMask,
+ OptionRouter,
+ OptionDomainName,
+ OptionDomainNameServer,
+ ),
)...)
}
diff --git a/dhcpv4/dhcpv4_test.go b/dhcpv4/dhcpv4_test.go
index eff7a71..a961f78 100644
--- a/dhcpv4/dhcpv4_test.go
+++ b/dhcpv4/dhcpv4_test.go
@@ -266,23 +266,22 @@ func TestDHCPv4NewRequestFromOfferWithModifier(t *testing.T) {
require.Equal(t, MessageTypeRequest, req.MessageType())
}
-func TestDHCPv4NewRenewFromOffer(t *testing.T) {
- offer, err := New()
+func TestDHCPv4NewRenewFromAck(t *testing.T) {
+ ack, err := New()
require.NoError(t, err)
- offer.SetBroadcast()
- offer.UpdateOption(OptMessageType(MessageTypeOffer))
- offer.UpdateOption(OptServerIdentifier(net.IPv4(192, 168, 0, 1)))
- offer.UpdateOption(OptRequestedIPAddress(net.IPv4(192, 168, 0, 1)))
- offer.YourIPAddr = net.IPv4(192, 168, 0, 1)
+ ack.SetBroadcast()
+ ack.UpdateOption(OptMessageType(MessageTypeAck))
+ ack.UpdateOption(OptServerIdentifier(net.IPv4(192, 168, 0, 1)))
+ ack.YourIPAddr = net.IPv4(192, 168, 0, 1)
- // RFC 2131: RENEW-style requests will be unicast
var req *DHCPv4
- req, err = NewRenewFromOffer(offer)
+ req, err = NewRenewFromAck(ack)
require.NoError(t, err)
require.Equal(t, MessageTypeRequest, req.MessageType())
require.Nil(t, req.GetOneOption(OptionServerIdentifier))
require.Nil(t, req.GetOneOption(OptionRequestedIPAddress))
- require.Equal(t, offer.YourIPAddr, req.ClientIPAddr)
+ require.Equal(t, ack.YourIPAddr, req.ClientIPAddr)
+ // RFC 2131: RENEW-style requests will be unicast
require.True(t, req.IsUnicast())
require.False(t, req.IsBroadcast())
// Renewals should behave identically to initial requests regarding requested options
@@ -292,12 +291,12 @@ func TestDHCPv4NewRenewFromOffer(t *testing.T) {
require.True(t, req.IsOptionRequested(OptionDomainNameServer))
}
-func TestDHCPv4NewRenewFromOfferWithModifier(t *testing.T) {
- offer, err := New()
+func TestDHCPv4NewRenewFromAckWithModifier(t *testing.T) {
+ ack, err := New()
require.NoError(t, err)
- offer.UpdateOption(OptMessageType(MessageTypeOffer))
+ ack.UpdateOption(OptMessageType(MessageTypeAck))
userClass := WithUserClass("linuxboot", false)
- req, err := NewRenewFromOffer(offer, userClass)
+ req, err := NewRenewFromAck(ack, userClass)
require.NoError(t, err)
require.Equal(t, MessageTypeRequest, req.MessageType())
require.Contains(t, req.UserClass(), "linuxboot")
@@ -355,6 +354,16 @@ func TestNewInform(t *testing.T) {
require.True(t, m.ClientIPAddr.Equal(localIP))
}
+func TestDHCPv4NewInformWithModifier(t *testing.T) {
+ hwAddr := net.HardwareAddr{1, 2, 3, 4, 5, 6}
+ localIP := net.IPv4(10, 10, 11, 11)
+ serverID := net.IPv4(192, 168, 0, 1)
+ m, err := NewInform(hwAddr, localIP, WithOption(OptServerIdentifier(serverID)))
+
+ require.NoError(t, err)
+ require.True(t, serverID.Equal(m.ServerIdentifier()))
+}
+
func TestIsOptionRequested(t *testing.T) {
pkt, err := New()
require.NoError(t, err)
diff --git a/dhcpv4/nclient4/client.go b/dhcpv4/nclient4/client.go
index d40e1a9..b4e4b56 100644
--- a/dhcpv4/nclient4/client.go
+++ b/dhcpv4/nclient4/client.go
@@ -478,6 +478,25 @@ func (c *Client) Request(ctx context.Context, modifiers ...dhcpv4.Modifier) (lea
return c.RequestFromOffer(ctx, offer, modifiers...)
}
+// Inform sends an INFORM request using the given local IP.
+// Returns the ACK response from the server on success.
+func (c *Client) Inform(ctx context.Context, localIP net.IP, modifiers ...dhcpv4.Modifier) (*dhcpv4.DHCPv4, error) {
+ request, err := dhcpv4.NewInform(c.ifaceHWAddr, localIP, modifiers...)
+ if err != nil {
+ return nil, err
+ }
+
+ // DHCP clients must not fill in the server identifier in an INFORM request as per RFC 2131 Section 4.4.1 Table 5,
+ // however, they may still unicast the request to the target server if the address is known (c.serverAddr), as per
+ // Section 4.4.3. The server must then respond with an ACK, as per Section 4.3.5.
+ response, err := c.SendAndRead(ctx, c.serverAddr, request, IsMessageType(dhcpv4.MessageTypeAck))
+ if err != nil {
+ return nil, fmt.Errorf("got an error while processing the request: %w", err)
+ }
+
+ return response, nil
+}
+
// ErrNak is returned if a DHCP server rejected our Request.
type ErrNak struct {
Offer *dhcpv4.DHCPv4
diff --git a/dhcpv4/nclient4/conn_unix.go b/dhcpv4/nclient4/conn_unix.go
index bece752..7f79f50 100644
--- a/dhcpv4/nclient4/conn_unix.go
+++ b/dhcpv4/nclient4/conn_unix.go
@@ -13,9 +13,9 @@ import (
"io"
"net"
- "github.com/mdlayher/ethernet"
- "github.com/mdlayher/raw"
+ "github.com/mdlayher/packet"
"github.com/u-root/uio/uio"
+ "golang.org/x/sys/unix"
)
var (
@@ -39,7 +39,7 @@ func NewRawUDPConn(iface string, port int) (net.PacketConn, error) {
if err != nil {
return nil, err
}
- rawConn, err := raw.ListenPacket(ifc, uint16(ethernet.EtherTypeIPv4), &raw.Config{LinuxSockDGRAM: true})
+ rawConn, err := packet.Listen(ifc, packet.Datagram, unix.ETH_P_IP, nil)
if err != nil {
return nil, err
}
@@ -152,8 +152,8 @@ func (upc *BroadcastRawUDPConn) WriteTo(b []byte, addr net.Addr) (int, error) {
}
// Using the boundAddr is not quite right here, but it works.
- packet := udp4pkt(b, udpAddr, upc.boundAddr)
+ pkt := udp4pkt(b, udpAddr, upc.boundAddr)
// Broadcasting is not always right, but hell, what the ARP do I know.
- return upc.PacketConn.WriteTo(packet, &raw.Addr{HardwareAddr: BroadcastMac})
+ return upc.PacketConn.WriteTo(pkt, &packet.Addr{HardwareAddr: BroadcastMac})
}
diff --git a/dhcpv4/nclient4/lease.go b/dhcpv4/nclient4/lease.go
index 1895dd0..8b1d9e6 100644
--- a/dhcpv4/nclient4/lease.go
+++ b/dhcpv4/nclient4/lease.go
@@ -42,15 +42,15 @@ func (c *Client) Release(lease *Lease, modifiers ...dhcpv4.Modifier) error {
// sourced from the initial offer in the lease, and the ACK of the lease is updated to the ACK of
// the latest renewal. This avoids issues with DHCP servers that omit information needed to build a
// completely new lease from their renewal ACK (such as the Windows DHCP Server).
-func (c *Client) Renew(ctx context.Context, lease *Lease, modifiers ...dhcpv4.Modifier) error {
+func (c *Client) Renew(ctx context.Context, lease *Lease, modifiers ...dhcpv4.Modifier) (*Lease, error) {
if lease == nil {
- return fmt.Errorf("lease is nil")
+ return nil, fmt.Errorf("lease is nil")
}
- request, err := dhcpv4.NewRenewFromOffer(lease.Offer, dhcpv4.PrependModifiers(modifiers,
+ request, err := dhcpv4.NewRenewFromAck(lease.ACK, dhcpv4.PrependModifiers(modifiers,
dhcpv4.WithOption(dhcpv4.OptMaxMessageSize(MaxMessageSize)))...)
if err != nil {
- return fmt.Errorf("unable to create a request: %w", err)
+ return nil, fmt.Errorf("unable to create a request: %w", err)
}
// Servers are supposed to only respond to Requests containing their server identifier,
@@ -61,17 +61,19 @@ func (c *Client) Renew(ctx context.Context, lease *Lease, modifiers ...dhcpv4.Mo
IsCorrectServer(lease.Offer.ServerIdentifier()),
IsMessageType(dhcpv4.MessageTypeAck, dhcpv4.MessageTypeNak)))
if err != nil {
- return fmt.Errorf("got an error while processing the request: %w", err)
+ return nil, fmt.Errorf("got an error while processing the request: %w", err)
}
if response.MessageType() == dhcpv4.MessageTypeNak {
- return &ErrNak{
+ return nil, &ErrNak{
Offer: lease.Offer,
Nak: response,
}
}
- // Update the ACK of the lease with the ACK of the latest renewal
- lease.ACK = response
-
- return nil
+ // Return a new lease with the latest ACK and updated creation time
+ return &Lease{
+ Offer: lease.Offer,
+ ACK: response,
+ CreationTime: time.Now(),
+ }, nil
}
diff --git a/dhcpv4/nclient4/lease_test.go b/dhcpv4/nclient4/lease_test.go
index d27eeca..d9377e7 100644
--- a/dhcpv4/nclient4/lease_test.go
+++ b/dhcpv4/nclient4/lease_test.go
@@ -238,7 +238,7 @@ func (sll *testServerLeaseList) runTest(t *testing.T) {
sll.lastTestSvrErrLock.RUnlock()
if keepgoing {
- err = clnt.Renew(context.Background(), lease)
+ lease, err = clnt.Renew(context.Background(), lease)
sll.lastTestSvrErrLock.RLock()
keepgoing = chkerr(err, sll.lastTestSvrErr, l.ShouldFail, t)
sll.lastTestSvrErrLock.RUnlock()
diff --git a/dhcpv4/server4/server_test.go b/dhcpv4/server4/server_test.go
index 7be868c..9fc44a3 100644
--- a/dhcpv4/server4/server_test.go
+++ b/dhcpv4/server4/server_test.go
@@ -110,15 +110,17 @@ func TestServer(t *testing.T) {
lease, err := c.Request(context.Background(), modifiers...)
require.NoError(t, err)
- require.NotNil(t, lease.Offer, lease.ACK)
+ require.NotNil(t, lease.Offer)
+ require.NotNil(t, lease.ACK)
for _, p := range []*dhcpv4.DHCPv4{lease.Offer, lease.ACK} {
require.Equal(t, xid, p.TransactionID)
require.Equal(t, ifaces[0].HardwareAddr, p.ClientHWAddr)
}
- err = c.Renew(context.Background(), lease, modifiers...)
+ lease, err = c.Renew(context.Background(), lease, modifiers...)
require.NoError(t, err)
- require.NotNil(t, lease.Offer, lease.ACK)
+ require.NotNil(t, lease.Offer)
+ require.NotNil(t, lease.ACK)
for _, p := range []*dhcpv4.DHCPv4{lease.Offer, lease.ACK} {
require.Equal(t, xid, p.TransactionID)
require.Equal(t, ifaces[0].HardwareAddr, p.ClientHWAddr)
diff --git a/dhcpv4/ztpv4/parse_circuitid.go b/dhcpv4/ztpv4/parse_circuitid.go
index 42f7647..63b34e3 100644
--- a/dhcpv4/ztpv4/parse_circuitid.go
+++ b/dhcpv4/ztpv4/parse_circuitid.go
@@ -37,6 +37,8 @@ var circuitRegexs = []*regexp.Regexp{
regexp.MustCompile("^Ethernet(?P<slot>[0-9]+)/(?P<port>[0-9]+)$"),
// Juniper bundle interface ae52.0
regexp.MustCompile("^ae(?P<port>[0-9]+).(?P<subport>[0-9])$"),
+ // Arista bundle interface Port-Channel1
+ regexp.MustCompile("^Port-Channel(?P<port>[0-9]+)$"),
// Ciena interface format
regexp.MustCompile(`\.OSC(-[0-9]+)?-(?P<slot>[0-9]+)-(?P<port>[0-9]+)$`),
}
diff --git a/dhcpv4/ztpv4/parse_circuitid_test.go b/dhcpv4/ztpv4/parse_circuitid_test.go
index 695d331..b2f6412 100644
--- a/dhcpv4/ztpv4/parse_circuitid_test.go
+++ b/dhcpv4/ztpv4/parse_circuitid_test.go
@@ -24,6 +24,7 @@ func TestMatchCircuitID(t *testing.T) {
{name: "Arista Vlan pattern 2", circuit: "Ethernet10:2020", want: &CircuitID{Port: "10", Vlan: "2020"}},
{name: "Cisco pattern", circuit: "Gi1/10:2020", want: &CircuitID{Slot: "1", Port: "10", Vlan: "2020"}},
{name: "Cisco Nexus pattern", circuit: "Ethernet1/3", want: &CircuitID{Slot: "1", Port: "3"}},
+ {name: "Arista Portchannel Pattern", circuit: "Port-Channel10", want: &CircuitID{Port: "10"}},
{name: "Juniper Bundle Pattern", circuit: "ae52.0", want: &CircuitID{Port: "52", SubPort: "0"}},
{name: "Juniper EX device pattern", circuit: "ge-0/0/0.0:RANDOMCHAR", want: &CircuitID{Slot: "0", Module: "0", Port: "0", SubPort: "0"}},
}
@@ -58,6 +59,7 @@ func TestFormatCircuitID(t *testing.T) {
{name: "Arista Vlan pattern 2", circuit: &CircuitID{Port: "10", Vlan: "2020"}, want: ",,10,,2020"},
{name: "Cisco Nexus pattern", circuit: &CircuitID{Slot: "1", Port: "3"}, want: "1,,3,,"},
{name: "Juniper Bundle Pattern", circuit: &CircuitID{Port: "52", SubPort: "0"}, want: ",,52,0,"},
+ {name: "Arista Portchannel Pattern", circuit: &CircuitID{Port: "10"}, want: ",,10,,"},
}
for _, tc := range tt {
@@ -87,6 +89,7 @@ func TestParseCircuitID(t *testing.T) {
{name: "Cisco pattern", circuit: []byte("Gi1/10:2020"), want: &CircuitID{Slot: "1", Port: "10", Vlan: "2020"}},
{name: "Cisco Nexus pattern", circuit: []byte("Ethernet1/3"), want: &CircuitID{Slot: "1", Port: "3"}},
{name: "Juniper Bundle Pattern", circuit: []byte("ae52.0"), want: &CircuitID{Port: "52", SubPort: "0"}},
+ {name: "Arista Portchannel Pattern", circuit: []byte("Port-Channel10"), want: &CircuitID{Port: "10"}},
{name: "Arista Vlan pattern 1 with circuitid type and length", circuit: []byte("\x00\x0fEthernet14:2001"), want: &CircuitID{Port: "14", Vlan: "2001"}},
{name: "juniperEX pattern", circuit: []byte("ge-0/0/0.0:RANDOMCHAR"), want: &CircuitID{Slot: "0", Module: "0", Port: "0", SubPort: "0"}},
{name: "Ciena pattern 1", circuit: []byte("tt-tt-tttt-6-7.OSC-1-2"), want: &CircuitID{Slot: "1", Port: "2"}},
diff --git a/dhcpv4/ztpv4/ztp.go b/dhcpv4/ztpv4/ztp.go
index ecb63c6..8ab3a8e 100644
--- a/dhcpv4/ztpv4/ztp.go
+++ b/dhcpv4/ztpv4/ztp.go
@@ -68,6 +68,16 @@ func parseClassIdentifier(packet *dhcpv4.DHCPv4) (*VendorData, error) {
vd.VendorName = p[0]
return vd, nil
+ // Juniper:tttt-ttt:DN817
+ case strings.HasPrefix(vc, "Juniper:"):
+ p := strings.Split(vc, ":")
+ if len(p) == 3 {
+ vd.VendorName = p[0]
+ vd.Model = p[1]
+ vd.Serial = p[2]
+ return vd, nil
+ }
+ return nil, fmt.Errorf("%w got '%s'", errVendorOptionMalformed, vc)
// For Ciena the class identifier (opt 60) is written in the following format:
// {vendor iana code}-{product}-{type}
diff --git a/dhcpv4/ztpv4/ztp_test.go b/dhcpv4/ztpv4/ztp_test.go
index 5d8ae8d..8210b12 100644
--- a/dhcpv4/ztpv4/ztp_test.go
+++ b/dhcpv4/ztpv4/ztp_test.go
@@ -41,6 +41,16 @@ func TestParseClassIdentifier(t *testing.T) {
want: &VendorData{VendorName: "Juniper", Model: "qfx10008", Serial: "DE123"},
},
{name: "juniperNoSerial", vc: "Juniper-qfx10008", fail: true},
+ {
+ name: "juniperHostnameSerialv2",
+ vc: "Juniper:ttttt-ttt:D12345",
+ want: &VendorData{VendorName: "Juniper", Model: "ttttt-ttt", Serial: "D12345"},
+ },
+ {
+ name: "juniperHostnameSerialv2Invalid",
+ vc: "Juniper:1",
+ fail: true,
+ },
{
name: "zpe",
vc: "ZPESystems:NSC:001234567",
diff --git a/dhcpv6/dhcpv6.go b/dhcpv6/dhcpv6.go
index 59d9733..45025ab 100644
--- a/dhcpv6/dhcpv6.go
+++ b/dhcpv6/dhcpv6.go
@@ -14,6 +14,7 @@ type DHCPv6 interface {
ToBytes() []byte
String() string
Summary() string
+ LongString(indent int) string
IsRelay() bool
// GetInnerMessage returns the innermost encapsulated DHCPv6 message.
@@ -46,7 +47,7 @@ func MessageFromBytes(data []byte) (*Message, error) {
}
buf.ReadBytes(d.TransactionID[:])
if buf.Error() != nil {
- return nil, fmt.Errorf("Error parsing DHCPv6 header: %v", buf.Error())
+ return nil, fmt.Errorf("failed to parse DHCPv6 header: %w", buf.Error())
}
if err := d.Options.FromBytes(buf.Data()); err != nil {
return nil, err
diff --git a/dhcpv6/dhcpv6_test.go b/dhcpv6/dhcpv6_test.go
index b1f952d..210f19d 100644
--- a/dhcpv6/dhcpv6_test.go
+++ b/dhcpv6/dhcpv6_test.go
@@ -165,9 +165,9 @@ func TestNewAdvertiseFromSolicit(t *testing.T) {
MessageType: MessageTypeSolicit,
TransactionID: TransactionID{0xa, 0xb, 0xc},
}
- s.AddOption(OptClientID(Duid{}))
+ s.AddOption(OptClientID(&DUIDLLT{}))
- a, err := NewAdvertiseFromSolicit(&s, WithServerID(Duid{}))
+ a, err := NewAdvertiseFromSolicit(&s, WithServerID(&DUIDLLT{}))
require.NoError(t, err)
require.Equal(t, a.TransactionID, s.TransactionID)
require.Equal(t, a.Type(), MessageTypeAdvertise)
@@ -178,35 +178,35 @@ func TestNewReplyFromMessage(t *testing.T) {
TransactionID: TransactionID{0xa, 0xb, 0xc},
MessageType: MessageTypeConfirm,
}
- var duid Duid
- msg.AddOption(OptClientID(duid))
- msg.AddOption(OptServerID(duid))
+ var duid DUIDLLT
+ msg.AddOption(OptClientID(&duid))
+ msg.AddOption(OptServerID(&duid))
- rep, err := NewReplyFromMessage(&msg, WithServerID(duid))
+ rep, err := NewReplyFromMessage(&msg, WithServerID(&duid))
require.NoError(t, err)
require.Equal(t, rep.TransactionID, msg.TransactionID)
require.Equal(t, rep.Type(), MessageTypeReply)
msg.MessageType = MessageTypeRenew
- rep, err = NewReplyFromMessage(&msg, WithServerID(duid))
+ rep, err = NewReplyFromMessage(&msg, WithServerID(&duid))
require.NoError(t, err)
require.Equal(t, rep.TransactionID, msg.TransactionID)
require.Equal(t, rep.Type(), MessageTypeReply)
msg.MessageType = MessageTypeRebind
- rep, err = NewReplyFromMessage(&msg, WithServerID(duid))
+ rep, err = NewReplyFromMessage(&msg, WithServerID(&duid))
require.NoError(t, err)
require.Equal(t, rep.TransactionID, msg.TransactionID)
require.Equal(t, rep.Type(), MessageTypeReply)
msg.MessageType = MessageTypeRelease
- rep, err = NewReplyFromMessage(&msg, WithServerID(duid))
+ rep, err = NewReplyFromMessage(&msg, WithServerID(&duid))
require.NoError(t, err)
require.Equal(t, rep.TransactionID, msg.TransactionID)
require.Equal(t, rep.Type(), MessageTypeReply)
msg.MessageType = MessageTypeInformationRequest
- rep, err = NewReplyFromMessage(&msg, WithServerID(duid))
+ rep, err = NewReplyFromMessage(&msg, WithServerID(&duid))
require.NoError(t, err)
require.Equal(t, rep.TransactionID, msg.TransactionID)
require.Equal(t, rep.Type(), MessageTypeReply)
@@ -226,9 +226,8 @@ func TestNewMessageTypeSolicit(t *testing.T) {
hwAddr, err := net.ParseMAC("24:0A:9E:9F:EB:2B")
require.NoError(t, err)
- duid := Duid{
- Type: DUID_LL,
- HwType: iana.HWTypeEthernet,
+ duid := &DUIDLL{
+ HWType: iana.HWTypeEthernet,
LinkLayerAddr: hwAddr,
}
@@ -239,7 +238,7 @@ func TestNewMessageTypeSolicit(t *testing.T) {
// Check CID
cduid := s.Options.ClientID()
require.NotNil(t, cduid)
- require.Equal(t, cduid, &duid)
+ require.Equal(t, cduid, duid)
// Check ORO
oro := s.Options.RequestedOptions()
diff --git a/dhcpv6/dhcpv6message.go b/dhcpv6/dhcpv6message.go
index e2210fd..9bb83bb 100644
--- a/dhcpv6/dhcpv6message.go
+++ b/dhcpv6/dhcpv6message.go
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net"
+ "strings"
"time"
"github.com/insomniacslk/dhcp/iana"
@@ -31,21 +32,21 @@ func (mo MessageOptions) ArchTypes() iana.Archs {
}
// ClientID returns the client identifier option.
-func (mo MessageOptions) ClientID() *Duid {
+func (mo MessageOptions) ClientID() DUID {
opt := mo.GetOne(OptionClientID)
if opt == nil {
return nil
}
- return &opt.(*optClientID).Duid
+ return opt.(*optClientID).DUID
}
// ServerID returns the server identifier option.
-func (mo MessageOptions) ServerID() *Duid {
+func (mo MessageOptions) ServerID() DUID {
opt := mo.GetOne(OptionServerID)
if opt == nil {
return nil
}
- return &opt.(*optServerID).Duid
+ return opt.(*optServerID).DUID
}
// IANA returns all Identity Association for Non-temporary Address options.
@@ -105,6 +106,18 @@ func (mo MessageOptions) OneIAPD() *OptIAPD {
return iapds[0]
}
+// FourRD returns all 4RD options.
+func (mo MessageOptions) FourRD() []*Opt4RD {
+ opts := mo.Get(Option4RD)
+ var frds []*Opt4RD
+ for _, o := range opts {
+ if m, ok := o.(*Opt4RD); ok {
+ frds = append(frds, m)
+ }
+ }
+ return frds
+}
+
// Status returns the status code associated with this option.
func (mo MessageOptions) Status() *OptStatusCode {
opt := mo.Options.GetOne(OptionStatusCode)
@@ -168,8 +181,8 @@ func (mo MessageOptions) BootFileURL() string {
if opt == nil {
return ""
}
- if u, ok := opt.(optBootFileURL); ok {
- return string(u)
+ if u, ok := opt.(*optBootFileURL); ok {
+ return u.url
}
return ""
}
@@ -180,8 +193,8 @@ func (mo MessageOptions) BootFileParam() []string {
if opt == nil {
return nil
}
- if u, ok := opt.(optBootFileParam); ok {
- return []string(u)
+ if u, ok := opt.(*optBootFileParam); ok {
+ return u.params
}
return nil
}
@@ -198,12 +211,39 @@ func (mo MessageOptions) UserClasses() [][]byte {
return nil
}
+// VendorClasses returns the all vendor class options.
+func (mo MessageOptions) VendorClasses() []*OptVendorClass {
+ opt := mo.Options.Get(OptionVendorClass)
+ if opt == nil {
+ return nil
+ }
+ var vo []*OptVendorClass
+ for _, o := range opt {
+ if t, ok := o.(*OptVendorClass); ok {
+ vo = append(vo, t)
+ }
+ }
+ return vo
+}
+
+// VendorClass returns the vendor class options matching the given enterprise
+// number.
+func (mo MessageOptions) VendorClass(enterpriseNumber uint32) [][]byte {
+ vo := mo.VendorClasses()
+ for _, v := range vo {
+ if v.EnterpriseNumber == enterpriseNumber {
+ return v.Data
+ }
+ }
+ return nil
+}
+
// VendorOpts returns the all vendor-specific options.
//
// RFC 8415 Section 21.17:
//
-// Multiple instances of the Vendor-specific Information option may appear in
-// a DHCP message.
+// Multiple instances of the Vendor-specific Information option may appear in
+// a DHCP message.
func (mo MessageOptions) VendorOpts() []*OptVendorOpts {
opt := mo.Options.Get(OptionVendorOpts)
if opt == nil {
@@ -222,8 +262,8 @@ func (mo MessageOptions) VendorOpts() []*OptVendorOpts {
//
// RFC 8415 Section 21.17:
//
-// Servers and clients MUST NOT send more than one instance of the
-// Vendor-specific Information option with the same Enterprise Number.
+// Servers and clients MUST NOT send more than one instance of the
+// Vendor-specific Information option with the same Enterprise Number.
func (mo MessageOptions) VendorOpt(enterpriseNumber uint32) Options {
vo := mo.VendorOpts()
for _, v := range vo {
@@ -346,9 +386,8 @@ func GetTime() uint32 {
// NewSolicit creates a new SOLICIT message, using the given hardware address to
// derive the IAID in the IA_NA option.
func NewSolicit(hwaddr net.HardwareAddr, modifiers ...Modifier) (*Message, error) {
- duid := Duid{
- Type: DUID_LLT,
- HwType: iana.HWTypeEthernet,
+ duid := &DUIDLLT{
+ HWType: iana.HWTypeEthernet,
Time: GetTime(),
LinkLayerAddr: hwaddr,
}
@@ -543,28 +582,33 @@ func (m *Message) IsOptionRequested(requested OptionCode) bool {
// String returns a short human-readable string for this message.
func (m *Message) String() string {
- return fmt.Sprintf("Message(messageType=%s transactionID=%s, %d options)",
+ return fmt.Sprintf("Message(MessageType=%s, TransactionID=%#x, %d options)",
m.MessageType, m.TransactionID, len(m.Options.Options))
}
// Summary prints all options associated with this message.
func (m *Message) Summary() string {
- ret := fmt.Sprintf(
- "Message\n"+
- " messageType=%s\n"+
- " transactionid=%s\n",
- m.MessageType,
- m.TransactionID,
- )
- ret += " options=["
- if len(m.Options.Options) > 0 {
- ret += "\n"
- }
- for _, opt := range m.Options.Options {
- ret += fmt.Sprintf(" %v\n", opt.String())
- }
- ret += " ]\n"
- return ret
+ return m.LongString(0)
+}
+
+// LongString prints all options associated with this message.
+func (m *Message) LongString(spaceIndent int) string {
+ indent := strings.Repeat(" ", spaceIndent)
+
+ var s strings.Builder
+ s.WriteString("Message{\n")
+ s.WriteString(indent)
+ s.WriteString(fmt.Sprintf(" MessageType=%s\n", m.MessageType))
+ s.WriteString(indent)
+ s.WriteString(fmt.Sprintf(" TransactionID=%s\n", m.TransactionID))
+ s.WriteString(indent)
+ s.WriteString(" Options: ")
+ s.WriteString(m.Options.Options.LongString(spaceIndent + 2))
+ s.WriteString("\n")
+ s.WriteString(indent)
+ s.WriteString("}")
+
+ return s.String()
}
// ToBytes returns the serialized version of this message as defined by RFC
diff --git a/dhcpv6/dhcpv6relay.go b/dhcpv6/dhcpv6relay.go
index 6245b81..5a29c87 100644
--- a/dhcpv6/dhcpv6relay.go
+++ b/dhcpv6/dhcpv6relay.go
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net"
+ "strings"
"github.com/insomniacslk/dhcp/iana"
"github.com/u-root/uio/uio"
@@ -94,29 +95,36 @@ func (r *RelayMessage) Type() MessageType {
// String prints a short human-readable relay message.
func (r *RelayMessage) String() string {
- ret := fmt.Sprintf(
- "RelayMessage(messageType=%s hopcount=%d, linkaddr=%s, peeraddr=%s, %d options)",
- r.Type(), r.HopCount, r.LinkAddr, r.PeerAddr, len(r.Options.Options),
- )
- return ret
+ return fmt.Sprintf("RelayMessage(MessageType=%s, HopCount=%d, LinkAddr=%s, PeerAddr=%s, %d options)",
+ r.Type(), r.HopCount, r.LinkAddr, r.PeerAddr, len(r.Options.Options))
}
// Summary prints all options associated with this relay message.
func (r *RelayMessage) Summary() string {
- ret := fmt.Sprintf(
- "RelayMessage\n"+
- " messageType=%v\n"+
- " hopcount=%v\n"+
- " linkaddr=%v\n"+
- " peeraddr=%v\n"+
- " options=%v\n",
- r.Type(),
- r.HopCount,
- r.LinkAddr,
- r.PeerAddr,
- r.Options,
- )
- return ret
+ return r.LongString(0)
+}
+
+// LongString prints all options associated with this message.
+func (r *RelayMessage) LongString(spaceIndent int) string {
+ indent := strings.Repeat(" ", spaceIndent)
+
+ var s strings.Builder
+ s.WriteString(indent)
+ s.WriteString("RelayMessage{\n")
+ s.WriteString(indent)
+ s.WriteString(fmt.Sprintf(" MessageType=%s\n", r.MessageType))
+ s.WriteString(indent)
+ s.WriteString(fmt.Sprintf(" HopCount=%d\n", r.HopCount))
+ s.WriteString(indent)
+ s.WriteString(fmt.Sprintf(" LinkAddr=%s\n", r.LinkAddr))
+ s.WriteString(indent)
+ s.WriteString(fmt.Sprintf(" PeerAddr=%s\n", r.PeerAddr))
+ s.WriteString(indent)
+ s.WriteString(" Options: ")
+ s.WriteString(r.Options.Options.LongString(spaceIndent + 2))
+ s.WriteString("\n}")
+
+ return s.String()
}
// ToBytes returns the serialized version of this relay message as defined by
diff --git a/dhcpv6/dhcpv6relay_test.go b/dhcpv6/dhcpv6relay_test.go
index 113842c..1d38855 100644
--- a/dhcpv6/dhcpv6relay_test.go
+++ b/dhcpv6/dhcpv6relay_test.go
@@ -96,7 +96,7 @@ func TestNewRelayRepFromRelayForw(t *testing.T) {
// create the inner message
s, err := NewMessage()
require.NoError(t, err)
- s.AddOption(OptClientID(Duid{}))
+ s.AddOption(OptClientID(&DUIDLL{}))
rf.AddOption(OptRelayMessage(s))
a, err := NewAdvertiseFromSolicit(s)
diff --git a/dhcpv6/duid.go b/dhcpv6/duid.go
index 2ae8e60..0470f9f 100644
--- a/dhcpv6/duid.go
+++ b/dhcpv6/duid.go
@@ -2,156 +2,294 @@ package dhcpv6
import (
"bytes"
- "encoding/binary"
"fmt"
"net"
"github.com/insomniacslk/dhcp/iana"
+ "github.com/u-root/uio/uio"
)
-// DuidType is the DUID type as defined in rfc3315.
-type DuidType uint16
+// DUID is the interface that all DUIDs adhere to.
+type DUID interface {
+ fmt.Stringer
-// DUID types
-const (
- DUID_LLT DuidType = 1
- DUID_EN DuidType = 2
- DUID_LL DuidType = 3
- DUID_UUID DuidType = 4
-)
+ ToBytes() []byte
+ FromBytes(p []byte) error
+ DUIDType() DUIDType
+ Equal(d DUID) bool
+}
-// DuidTypeToString maps a DuidType to a name.
-var DuidTypeToString = map[DuidType]string{
- DUID_LL: "DUID-LL",
- DUID_LLT: "DUID-LLT",
- DUID_EN: "DUID-EN",
- DUID_UUID: "DUID-UUID",
+// DUIDLLT is a DUID based on link-layer address plus time (RFC 8415 Section 11.2).
+type DUIDLLT struct {
+ HWType iana.HWType
+ Time uint32
+ LinkLayerAddr net.HardwareAddr
}
-func (d DuidType) String() string {
- if dtype, ok := DuidTypeToString[d]; ok {
- return dtype
+// String pretty-prints DUIDLLT information.
+func (d DUIDLLT) String() string {
+ return fmt.Sprintf("DUID-LLT{HWType=%s HWAddr=%s Time=%d}", d.HWType, d.LinkLayerAddr, d.Time)
+}
+
+// DUIDType returns the DUID_LLT type.
+func (d DUIDLLT) DUIDType() DUIDType {
+ return DUID_LLT
+}
+
+// ToBytes serializes the option out to bytes.
+func (d DUIDLLT) ToBytes() []byte {
+ buf := uio.NewBigEndianBuffer(nil)
+ buf.Write16(uint16(d.DUIDType()))
+ buf.Write16(uint16(d.HWType))
+ buf.Write32(d.Time)
+ buf.WriteBytes(d.LinkLayerAddr)
+ return buf.Data()
+}
+
+// FromBytes reads the option.
+func (d *DUIDLLT) FromBytes(p []byte) error {
+ buf := uio.NewBigEndianBuffer(p)
+ d.HWType = iana.HWType(buf.Read16())
+ d.Time = buf.Read32()
+ d.LinkLayerAddr = buf.ReadAll()
+ return buf.FinError()
+}
+
+// Equal returns true if e is a DUID-LLT with the same values as d.
+func (d *DUIDLLT) Equal(e DUID) bool {
+ ellt, ok := e.(*DUIDLLT)
+ if !ok {
+ return false
}
- return "Unknown"
-}
-
-// Duid is a DHCP Unique Identifier.
-type Duid struct {
- Type DuidType
- HwType iana.HWType // for DUID-LLT and DUID-LL. Ignored otherwise. RFC 826
- Time uint32 // for DUID-LLT. Ignored otherwise
- LinkLayerAddr net.HardwareAddr
- EnterpriseNumber uint32 // for DUID-EN. Ignored otherwise
- EnterpriseIdentifier []byte // for DUID-EN. Ignored otherwise
- Uuid []byte // for DUID-UUID. Ignored otherwise
- Opaque []byte // for unknown DUIDs
-}
-
-// Length returns the DUID length in bytes.
-func (d *Duid) Length() int {
- if d.Type == DUID_LLT {
- return 8 + len(d.LinkLayerAddr)
- } else if d.Type == DUID_LL {
- return 4 + len(d.LinkLayerAddr)
- } else if d.Type == DUID_EN {
- return 6 + len(d.EnterpriseIdentifier)
- } else if d.Type == DUID_UUID {
- return 18
- } else {
- return 2 + len(d.Opaque)
+ if d == nil {
+ return d == ellt
}
+ return d.HWType == ellt.HWType && d.Time == ellt.Time && bytes.Equal(d.LinkLayerAddr, ellt.LinkLayerAddr)
+}
+
+// DUIDLL is a DUID based on link-layer (RFC 8415 Section 11.4).
+type DUIDLL struct {
+ HWType iana.HWType
+ LinkLayerAddr net.HardwareAddr
+}
+
+// String pretty-prints DUIDLL information.
+func (d DUIDLL) String() string {
+ return fmt.Sprintf("DUID-LL{HWType=%s HWAddr=%s}", d.HWType, d.LinkLayerAddr)
}
-// Equal compares two Duid objects.
-func (d Duid) Equal(o Duid) bool {
- if d.Type != o.Type ||
- d.HwType != o.HwType ||
- d.Time != o.Time ||
- !bytes.Equal(d.LinkLayerAddr, o.LinkLayerAddr) ||
- d.EnterpriseNumber != o.EnterpriseNumber ||
- !bytes.Equal(d.EnterpriseIdentifier, o.EnterpriseIdentifier) ||
- !bytes.Equal(d.Uuid, o.Uuid) ||
- !bytes.Equal(d.Opaque, o.Opaque) {
+// DUIDType returns the DUID_LL type.
+func (d DUIDLL) DUIDType() DUIDType {
+ return DUID_LL
+}
+
+// ToBytes serializes the option out to bytes.
+func (d DUIDLL) ToBytes() []byte {
+ buf := uio.NewBigEndianBuffer(nil)
+ buf.Write16(uint16(d.DUIDType()))
+ buf.Write16(uint16(d.HWType))
+ buf.WriteBytes(d.LinkLayerAddr)
+ return buf.Data()
+}
+
+// FromBytes reads the option.
+func (d *DUIDLL) FromBytes(p []byte) error {
+ buf := uio.NewBigEndianBuffer(p)
+ d.HWType = iana.HWType(buf.Read16())
+ d.LinkLayerAddr = buf.ReadAll()
+ return buf.FinError()
+}
+
+// Equal returns true if e is a DUID-LL with the same values as d.
+func (d *DUIDLL) Equal(e DUID) bool {
+ ell, ok := e.(*DUIDLL)
+ if !ok {
return false
}
- return true
-}
-
-// ToBytes serializes a Duid object.
-func (d *Duid) ToBytes() []byte {
- if d.Type == DUID_LLT {
- buf := make([]byte, 8)
- binary.BigEndian.PutUint16(buf[0:2], uint16(d.Type))
- binary.BigEndian.PutUint16(buf[2:4], uint16(d.HwType))
- binary.BigEndian.PutUint32(buf[4:8], d.Time)
- return append(buf, d.LinkLayerAddr...)
- } else if d.Type == DUID_LL {
- buf := make([]byte, 4)
- binary.BigEndian.PutUint16(buf[0:2], uint16(d.Type))
- binary.BigEndian.PutUint16(buf[2:4], uint16(d.HwType))
- return append(buf, d.LinkLayerAddr...)
- } else if d.Type == DUID_EN {
- buf := make([]byte, 6)
- binary.BigEndian.PutUint16(buf[0:2], uint16(d.Type))
- binary.BigEndian.PutUint32(buf[2:6], d.EnterpriseNumber)
- return append(buf, d.EnterpriseIdentifier...)
- } else if d.Type == DUID_UUID {
- buf := make([]byte, 2)
- binary.BigEndian.PutUint16(buf[0:2], uint16(d.Type))
- return append(buf, d.Uuid...)
- } else {
- buf := make([]byte, 2)
- binary.BigEndian.PutUint16(buf[0:2], uint16(d.Type))
- return append(buf, d.Opaque...)
+ if d == nil {
+ return d == ell
}
+ return d.HWType == ell.HWType && bytes.Equal(d.LinkLayerAddr, ell.LinkLayerAddr)
+}
+
+// DUIDEN is a DUID based on enterprise number (RFC 8415 Section 11.3).
+type DUIDEN struct {
+ EnterpriseNumber uint32
+ EnterpriseIdentifier []byte
+}
+
+// String pretty-prints DUIDEN information.
+func (d DUIDEN) String() string {
+ return fmt.Sprintf("DUID-EN{EnterpriseNumber=%d EnterpriseIdentifier=%s}", d.EnterpriseNumber, d.EnterpriseIdentifier)
+}
+
+// DUIDType returns the DUID_EN type.
+func (d DUIDEN) DUIDType() DUIDType {
+ return DUID_EN
+}
+
+// ToBytes serializes the option out to bytes.
+func (d DUIDEN) ToBytes() []byte {
+ buf := uio.NewBigEndianBuffer(nil)
+ buf.Write16(uint16(d.DUIDType()))
+ buf.Write32(d.EnterpriseNumber)
+ buf.WriteBytes(d.EnterpriseIdentifier)
+ return buf.Data()
}
-func (d *Duid) String() string {
- var hwaddr string
- if d.HwType == iana.HWTypeEthernet {
- for _, b := range d.LinkLayerAddr {
- hwaddr += fmt.Sprintf("%02x:", b)
- }
- if len(hwaddr) > 0 && hwaddr[len(hwaddr)-1] == ':' {
- hwaddr = hwaddr[:len(hwaddr)-1]
- }
+// FromBytes reads the option.
+func (d *DUIDEN) FromBytes(p []byte) error {
+ buf := uio.NewBigEndianBuffer(p)
+ d.EnterpriseNumber = buf.Read32()
+ d.EnterpriseIdentifier = buf.ReadAll()
+ return buf.FinError()
+}
+
+// Equal returns true if e is a DUID-EN with the same values as d.
+func (d *DUIDEN) Equal(e DUID) bool {
+ en, ok := e.(*DUIDEN)
+ if !ok {
+ return false
}
- return fmt.Sprintf("DUID{type=%v hwtype=%v hwaddr=%v}", d.Type.String(), d.HwType.String(), hwaddr)
+ if d == nil {
+ return d == en
+ }
+ return d.EnterpriseNumber == en.EnterpriseNumber && bytes.Equal(d.EnterpriseIdentifier, en.EnterpriseIdentifier)
+}
+
+// DUIDUUID is a DUID based on UUID (RFC 8415 Section 11.5).
+type DUIDUUID struct {
+ // Defined by RFC 6355.
+ UUID [16]byte
+}
+
+// String pretty-prints DUIDUUID information.
+func (d DUIDUUID) String() string {
+ return fmt.Sprintf("DUID-UUID{%#x}", d.UUID[:])
}
-// DuidFromBytes parses a Duid from a byte slice.
-func DuidFromBytes(data []byte) (*Duid, error) {
- if len(data) < 2 {
- return nil, fmt.Errorf("Invalid DUID: shorter than 2 bytes")
+// DUIDType returns the DUID_UUID type.
+func (d DUIDUUID) DUIDType() DUIDType {
+ return DUID_UUID
+}
+
+// ToBytes serializes the option out to bytes.
+func (d DUIDUUID) ToBytes() []byte {
+ buf := uio.NewBigEndianBuffer(nil)
+ buf.Write16(uint16(d.DUIDType()))
+ buf.WriteData(d.UUID[:])
+ return buf.Data()
+}
+
+// FromBytes reads the option.
+func (d *DUIDUUID) FromBytes(p []byte) error {
+ if len(p) != 16 {
+ return fmt.Errorf("buffer is length %d, DUID-UUID must be exactly 16 bytes", len(p))
}
- d := Duid{}
- d.Type = DuidType(binary.BigEndian.Uint16(data[0:2]))
- if d.Type == DUID_LLT {
- if len(data) < 8 {
- return nil, fmt.Errorf("Invalid DUID-LLT: shorter than 8 bytes")
- }
- d.HwType = iana.HWType(binary.BigEndian.Uint16(data[2:4]))
- d.Time = binary.BigEndian.Uint32(data[4:8])
- d.LinkLayerAddr = data[8:]
- } else if d.Type == DUID_LL {
- if len(data) < 4 {
- return nil, fmt.Errorf("Invalid DUID-LL: shorter than 4 bytes")
- }
- d.HwType = iana.HWType(binary.BigEndian.Uint16(data[2:4]))
- d.LinkLayerAddr = data[4:]
- } else if d.Type == DUID_EN {
- if len(data) < 6 {
- return nil, fmt.Errorf("Invalid DUID-EN: shorter than 6 bytes")
- }
- d.EnterpriseNumber = binary.BigEndian.Uint32(data[2:6])
- d.EnterpriseIdentifier = data[6:]
- } else if d.Type == DUID_UUID {
- if len(data) != 18 {
- return nil, fmt.Errorf("Invalid DUID-UUID length. Expected 18, got %v", len(data))
- }
- d.Uuid = data[2:18]
- } else {
- d.Opaque = data[2:]
+ copy(d.UUID[:], p)
+ return nil
+}
+
+// Equal returns true if e is a DUID-UUID with the same values as d.
+func (d *DUIDUUID) Equal(e DUID) bool {
+ euuid, ok := e.(*DUIDUUID)
+ if !ok {
+ return false
+ }
+ if d == nil {
+ return d == euuid
+ }
+ return d.UUID == euuid.UUID
+}
+
+// DUIDOpaque is a DUID of unknown type.
+type DUIDOpaque struct {
+ Type DUIDType
+ Data []byte
+}
+
+// String pretty-prints opaque DUID information.
+func (d DUIDOpaque) String() string {
+ return fmt.Sprintf("DUID-Opaque{Type=%d Data=%#x}", d.Type, d.Data)
+}
+
+// DUIDType returns the opaque DUID type.
+func (d DUIDOpaque) DUIDType() DUIDType {
+ return d.Type
+}
+
+// ToBytes serializes the option out to bytes.
+func (d DUIDOpaque) ToBytes() []byte {
+ buf := uio.NewBigEndianBuffer(nil)
+ buf.Write16(uint16(d.Type))
+ buf.WriteData(d.Data)
+ return buf.Data()
+}
+
+// FromBytes reads the option.
+func (d *DUIDOpaque) FromBytes(p []byte) error {
+ d.Data = append([]byte(nil), p...)
+ return nil
+}
+
+// Equal returns true if e is an opaque DUID with the same values as d.
+func (d *DUIDOpaque) Equal(e DUID) bool {
+ eopaque, ok := e.(*DUIDOpaque)
+ if !ok {
+ return false
+ }
+ if d == nil {
+ return d == eopaque
+ }
+ return d.Type == eopaque.Type && bytes.Equal(d.Data, eopaque.Data)
+}
+
+// DUIDType is the DUID type as defined in RFC 3315.
+type DUIDType uint16
+
+// DUID types
+const (
+ DUID_LLT DUIDType = 1
+ DUID_EN DUIDType = 2
+ DUID_LL DUIDType = 3
+ DUID_UUID DUIDType = 4
+)
+
+// duidTypeToString maps a DUIDType to a name.
+var duidTypeToString = map[DUIDType]string{
+ DUID_LL: "DUID-LL",
+ DUID_LLT: "DUID-LLT",
+ DUID_EN: "DUID-EN",
+ DUID_UUID: "DUID-UUID",
+}
+
+func (d DUIDType) String() string {
+ if dtype, ok := duidTypeToString[d]; ok {
+ return dtype
+ }
+ return "unknown"
+}
+
+// DUIDFromBytes parses a DUID from a byte slice.
+func DUIDFromBytes(data []byte) (DUID, error) {
+ buf := uio.NewBigEndianBuffer(data)
+ if !buf.Has(2) {
+ return nil, fmt.Errorf("%w: have %d bytes, want 2 bytes", uio.ErrBufferTooShort, buf.Len())
+ }
+
+ typ := DUIDType(buf.Read16())
+ var d DUID
+ switch typ {
+ case DUID_LLT:
+ d = &DUIDLLT{}
+ case DUID_LL:
+ d = &DUIDLL{}
+ case DUID_EN:
+ d = &DUIDEN{}
+ case DUID_UUID:
+ d = &DUIDUUID{}
+ default:
+ d = &DUIDOpaque{Type: typ}
}
- return &d, nil
+ return d, d.FromBytes(buf.Data())
}
diff --git a/dhcpv6/duid_test.go b/dhcpv6/duid_test.go
index 5efa0e1..7174dc5 100644
--- a/dhcpv6/duid_test.go
+++ b/dhcpv6/duid_test.go
@@ -3,6 +3,7 @@ package dhcpv6
import (
"bytes"
"net"
+ "reflect"
"testing"
"github.com/insomniacslk/dhcp/iana"
@@ -11,138 +12,225 @@ import (
func TestDuidInvalidTooShort(t *testing.T) {
// too short DUID at all (must be at least 2 bytes)
- _, err := DuidFromBytes([]byte{0})
+ _, err := DUIDFromBytes([]byte{0})
require.Error(t, err)
// too short DUID_LL (must be at least 4 bytes)
- _, err = DuidFromBytes([]byte{0, 3, 0xa})
+ _, err = DUIDFromBytes([]byte{0, 3, 0xa})
require.Error(t, err)
// too short DUID_EN (must be at least 6 bytes)
- _, err = DuidFromBytes([]byte{0, 2, 0xa, 0xb, 0xc})
+ _, err = DUIDFromBytes([]byte{0, 2, 0xa, 0xb, 0xc})
require.Error(t, err)
// too short DUID_LLT (must be at least 8 bytes)
- _, err = DuidFromBytes([]byte{0, 1, 0xa, 0xb, 0xc, 0xd, 0xe})
+ _, err = DUIDFromBytes([]byte{0, 1, 0xa, 0xb, 0xc, 0xd, 0xe})
require.Error(t, err)
// too short DUID_UUID (must be at least 18 bytes)
- _, err = DuidFromBytes([]byte{0, 4, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf})
+ _, err = DUIDFromBytes([]byte{0, 4, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf})
require.Error(t, err)
}
-func TestDuidLLTFromBytes(t *testing.T) {
- buf := []byte{
- 0, 1, // DUID_LLT
- 0, 1, // HwTypeEthernet
- 0x01, 0x02, 0x03, 0x04, // time
- 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, // link-layer addr
- }
- duid, err := DuidFromBytes(buf)
- require.NoError(t, err)
- require.Equal(t, 14, duid.Length())
- require.Equal(t, DUID_LLT, duid.Type)
- require.Equal(t, uint32(0x01020304), duid.Time)
- require.Equal(t, iana.HWTypeEthernet, duid.HwType)
- require.Equal(t, net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, duid.LinkLayerAddr)
-}
+func TestFromBytes(t *testing.T) {
+ for _, tt := range []struct {
+ name string
+ buf []byte
+ want DUID
+ stringer string
+ }{
+ {
+ name: "DUID-LLT",
+ buf: []byte{
+ 0, 1, // DUID_LLT
+ 0, 1, // HwTypeEthernet
+ 0x01, 0x02, 0x03, 0x04, // time
+ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, // link-layer addr
+ },
+ want: &DUIDLLT{
+ Time: 0x01020304,
+ HWType: iana.HWTypeEthernet,
+ LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
+ },
+ stringer: "DUID-LLT{HWType=Ethernet HWAddr=aa:bb:cc:dd:ee:ff Time=16909060}",
+ },
+ {
+ name: "DUID-LL",
+ buf: []byte{
+ 0, 3, // DUID_LL
+ 0, 1, // HwTypeEthernet
+ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, // link-layer addr
+ },
+ want: &DUIDLL{
+ HWType: iana.HWTypeEthernet,
+ LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
+ },
+ stringer: "DUID-LL{HWType=Ethernet HWAddr=aa:bb:cc:dd:ee:ff}",
+ },
+ {
+ name: "DUID-EN",
+ buf: []byte{
+ 0, 2, // DUID_EN
+ 0, 0, 0, 1, // EnterpriseNumber
+ 0x66, 0x6f, 0x6f, // "foo"
+ },
+ want: &DUIDEN{
+ EnterpriseNumber: 0x1,
+ EnterpriseIdentifier: []byte("foo"),
+ },
+ stringer: "DUID-EN{EnterpriseNumber=1 EnterpriseIdentifier=foo}",
+ },
+ {
+ name: "DUID-UUID",
+ buf: []byte{
+ 0x00, 0x04, // DUID_UUID
+ 0x01, 0x02, 0x03, 0x04, // UUID
+ 0x01, 0x02, 0x03, 0x04, // UUID
+ 0x01, 0x02, 0x03, 0x04, // UUID
+ 0x01, 0x02, 0x03, 0x04, // UUID
+ },
+ want: &DUIDUUID{
+ UUID: [16]byte{
+ 0x01, 0x02, 0x03, 0x04,
+ 0x01, 0x02, 0x03, 0x04,
+ 0x01, 0x02, 0x03, 0x04,
+ 0x01, 0x02, 0x03, 0x04,
+ },
+ },
+ stringer: "DUID-UUID{0x01020304010203040102030401020304}",
+ },
+ {
+ name: "DUIDOpaque",
+ buf: []byte{
+ 0x00, 0x05, // unknown DUID
+ 0x01, 0x02, 0x03, // Opaque
+ },
+ want: &DUIDOpaque{
+ Type: 0x5,
+ Data: []byte{0x01, 0x02, 0x03},
+ },
+ stringer: "DUID-Opaque{Type=5 Data=0x010203}",
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ // FromBytes
+ got, err := DUIDFromBytes(tt.buf)
+ if err != nil {
+ t.Errorf("DUIDFromBytes = %v", err)
+ }
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("DUIDFromBytes = %v, want %v", got, tt.want)
+ }
-func TestDuidLLFromBytes(t *testing.T) {
- buf := []byte{
- 0, 3, // DUID_LL
- 0, 1, // HwTypeEthernet
- 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, // link-layer addr
- }
- duid, err := DuidFromBytes(buf)
- require.NoError(t, err)
- require.Equal(t, 10, duid.Length())
- require.Equal(t, DUID_LL, duid.Type)
- require.Equal(t, iana.HWTypeEthernet, duid.HwType)
- require.Equal(t, net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, duid.LinkLayerAddr)
-}
+ // ToBytes
+ buf := tt.want.ToBytes()
+ if !bytes.Equal(buf, tt.buf) {
+ t.Errorf("ToBytes() = %#x, want %#x", buf, tt.buf)
+ }
-func TestDuidUuidFromBytes(t *testing.T) {
- buf := []byte{
- 0x00, 0x04, // DUID_UUID
+ // Stringer
+ s := tt.want.String()
+ if s != tt.stringer {
+ t.Errorf("String() = %s, want %s", s, tt.stringer)
+ }
+ })
}
- uuid := []byte{0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00, 0x07, 0x00, 0x08}
- buf = append(buf, uuid...)
- duid, err := DuidFromBytes(buf)
- require.NoError(t, err)
- require.Equal(t, 18, duid.Length())
- require.Equal(t, DUID_UUID, duid.Type)
- require.Equal(t, uuid, duid.Uuid)
}
-func TestDuidLLTToBytes(t *testing.T) {
- expected := []byte{
- 0, 1, // DUID_LLT
- 0, 1, // HwTypeEthernet
- 0x01, 0x02, 0x03, 0x04, // time
- 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, // link-layer addr
- }
- duid := Duid{
- Type: DUID_LLT,
- HwType: iana.HWTypeEthernet,
- Time: uint32(0x01020304),
- LinkLayerAddr: []byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
- }
- toBytes := duid.ToBytes()
- require.Equal(t, expected, toBytes)
-}
-
-func TestDuidUuidToBytes(t *testing.T) {
- uuid := []byte{0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00, 0x07, 0x00, 0x08, 0x00, 0x09}
- expected := []byte{00, 04}
- expected = append(expected, uuid...)
- duid := Duid{
- Type: DUID_UUID,
- Uuid: uuid,
- }
- toBytes := duid.ToBytes()
- if !bytes.Equal(toBytes, expected) {
- t.Fatalf("Invalid ToBytes result. Expected %v, got %v", expected, toBytes)
- }
-}
+func TestEqual(t *testing.T) {
+ for _, tt := range []struct {
+ name string
+ a DUID
+ b DUID
+ want bool
+ }{
+ {
+ name: "DUID-LL-equal",
+ a: &DUIDLL{HWType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}},
+ b: &DUIDLL{HWType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}},
+ want: true,
+ },
+ {
+ name: "DUID-LL-not-equal",
+ a: &DUIDLL{HWType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}},
+ b: &DUIDLL{HWType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr{0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa}},
+ want: false,
+ },
+ {
+ name: "DUID-LL-and-DUID-EN",
+ a: &DUIDLL{HWType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}},
+ b: &DUIDEN{EnterpriseNumber: 5, EnterpriseIdentifier: []byte("foo")},
+ want: false,
+ },
+ {
+ name: "DUID-EN-equal",
+ a: &DUIDEN{EnterpriseNumber: 5, EnterpriseIdentifier: []byte("foo")},
+ b: &DUIDEN{EnterpriseNumber: 5, EnterpriseIdentifier: []byte("foo")},
+ want: true,
+ },
+ {
+ name: "DUID-EN-not-equal",
+ a: &DUIDEN{EnterpriseNumber: 5, EnterpriseIdentifier: []byte("foo")},
+ b: &DUIDEN{EnterpriseNumber: 5, EnterpriseIdentifier: []byte("bar")},
+ want: false,
+ },
+ {
+ name: "DUID-LLT-equal",
+ a: &DUIDLLT{HWType: iana.HWTypeEthernet, Time: 10, LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}},
+ b: &DUIDLLT{HWType: iana.HWTypeEthernet, Time: 10, LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}},
+ want: true,
+ },
+ {
+ name: "DUID-LLT-not-equal",
+ a: &DUIDLLT{HWType: iana.HWTypeEthernet, Time: 10, LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}},
+ b: &DUIDLLT{HWType: iana.HWTypeEthernet, Time: 10, LinkLayerAddr: net.HardwareAddr{0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa}},
+ want: false,
+ },
+ {
+ name: "DUID-LLT-and-DUID-UUID",
+ a: &DUIDLLT{HWType: iana.HWTypeEthernet, Time: 10, LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}},
+ b: &DUIDUUID{UUID: [16]byte{0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04}},
+ want: false,
+ },
+ {
+ name: "DUID-UUID-equal",
+ a: &DUIDUUID{UUID: [16]byte{0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04}},
+ b: &DUIDUUID{UUID: [16]byte{0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04}},
+ want: true,
+ },
+ {
+ name: "DUID-UUID-not-equal",
+ a: &DUIDUUID{UUID: [16]byte{0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04}},
+ b: &DUIDUUID{UUID: [16]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}},
+ want: false,
+ },
+ {
+ name: "DUID-UUID-and-DUID-Opaque",
+ a: &DUIDOpaque{Type: 5, Data: []byte{0x1}},
+ b: &DUIDUUID{UUID: [16]byte{0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04}},
+ want: false,
+ },
+ {
+ name: "DUID-Opaque-equal",
+ a: &DUIDOpaque{Type: 5, Data: []byte{0x1}},
+ b: &DUIDOpaque{Type: 5, Data: []byte{0x1}},
+ want: true,
+ },
+ {
+ name: "DUID-Opaque-not-equal",
+ a: &DUIDOpaque{Type: 5, Data: []byte{0x1}},
+ b: &DUIDOpaque{Type: 5, Data: []byte{0x2}},
+ want: false,
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.a.Equal(tt.b); got != tt.want {
+ t.Errorf("%s.Equal(%s) = %v, want %v", tt.a, tt.b, got, tt.want)
+ }
-func TestOpaqueDuid(t *testing.T) {
- duid := []byte("\x00\x0a\x00\x03\x00\x01\x4c\x5e\x0c\x43\xbf\x39")
- d, err := DuidFromBytes(duid)
- if err != nil {
- t.Fatalf("DuidFromBytes: unexpected error: %v", err)
- }
- if got, want := d.Length(), len(duid); got != want {
- t.Errorf("Length: unexpected result: got %d, want %d", got, want)
- }
- if got, want := d.ToBytes(), duid; !bytes.Equal(got, want) {
- t.Fatalf("ToBytes: unexpected result: got %x, want %x", got, want)
- }
-}
-
-func TestDuidEqual(t *testing.T) {
- d := Duid{
- Type: DUID_LL,
- HwType: iana.HWTypeEthernet,
- LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
- }
- o := Duid{
- Type: DUID_LL,
- HwType: iana.HWTypeEthernet,
- LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
- }
- require.True(t, d.Equal(o))
-}
-
-func TestDuidEqualNotEqual(t *testing.T) {
- d := Duid{
- Type: DUID_LL,
- HwType: iana.HWTypeEthernet,
- LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
- }
- o := Duid{
- Type: DUID_LL,
- HwType: iana.HWTypeEthernet,
- LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x00},
+ if got := tt.b.Equal(tt.a); got != tt.want {
+ t.Errorf("%s.Equal(%s) = %v, want %v", tt.b, tt.a, got, tt.want)
+ }
+ })
}
- require.False(t, d.Equal(o))
}
diff --git a/dhcpv6/iputils.go b/dhcpv6/iputils.go
index d2cac47..c4926e1 100644
--- a/dhcpv6/iputils.go
+++ b/dhcpv6/iputils.go
@@ -90,8 +90,15 @@ func ExtractMAC(packet DHCPv6) (net.HardwareAddr, error) {
if duid == nil {
return nil, fmt.Errorf("client ID not found in packet")
}
- if duid.LinkLayerAddr == nil {
- return nil, fmt.Errorf("failed to extract MAC")
+ switch d := duid.(type) {
+ case *DUIDLL:
+ if d.LinkLayerAddr != nil {
+ return d.LinkLayerAddr, nil
+ }
+ case *DUIDLLT:
+ if d.LinkLayerAddr != nil {
+ return d.LinkLayerAddr, nil
+ }
}
- return duid.LinkLayerAddr, nil
+ return nil, fmt.Errorf("failed to extract MAC")
}
diff --git a/dhcpv6/iputils_test.go b/dhcpv6/iputils_test.go
index e206aa1..601e96e 100644
--- a/dhcpv6/iputils_test.go
+++ b/dhcpv6/iputils_test.go
@@ -131,10 +131,9 @@ func Test_ExtractMAC(t *testing.T) {
require.NoError(t, err)
require.Equal(t, mac.String(), "24:8a:07:56:dc:a4")
- // MAC extracted from DUID
- duid := Duid{
- Type: DUID_LL,
- HwType: iana.HWTypeEthernet,
+ // MAC extracted from DUID-LL
+ duid := &DUIDLL{
+ HWType: iana.HWTypeEthernet,
LinkLayerAddr: []byte{0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa},
}
solicit, err := NewMessage(WithClientID(duid))
@@ -145,6 +144,18 @@ func Test_ExtractMAC(t *testing.T) {
require.NoError(t, err)
require.Equal(t, mac.String(), "aa:aa:aa:aa:aa:aa")
+ // MAC extracted from DUID-LLT
+ solicit, err = NewMessage(WithClientID(&DUIDLLT{
+ HWType: iana.HWTypeEthernet,
+ LinkLayerAddr: []byte{0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa},
+ }))
+ require.NoError(t, err)
+ relay, err = EncapsulateRelay(solicit, MessageTypeRelayForward, net.IPv6zero, net.IPv6zero)
+ require.NoError(t, err)
+ mac, err = ExtractMAC(relay)
+ require.NoError(t, err)
+ require.Equal(t, mac.String(), "aa:aa:aa:aa:aa:aa")
+
// no client ID
solicit, err = NewMessage()
require.NoError(t, err)
@@ -152,8 +163,8 @@ func Test_ExtractMAC(t *testing.T) {
require.Error(t, err)
// DUID is not DuidLL or DuidLLT
- duid = Duid{}
- solicit, err = NewMessage(WithClientID(duid))
+ duiden := &DUIDEN{}
+ solicit, err = NewMessage(WithClientID(duiden))
require.NoError(t, err)
_, err = ExtractMAC(solicit)
require.Error(t, err)
diff --git a/dhcpv6/modifiers.go b/dhcpv6/modifiers.go
index 1d8c49f..b0d22c5 100644
--- a/dhcpv6/modifiers.go
+++ b/dhcpv6/modifiers.go
@@ -16,12 +16,12 @@ func WithOption(o Option) Modifier {
}
// WithClientID adds a client ID option to a DHCPv6 packet
-func WithClientID(duid Duid) Modifier {
+func WithClientID(duid DUID) Modifier {
return WithOption(OptClientID(duid))
}
// WithServerID adds a client ID option to a DHCPv6 packet
-func WithServerID(duid Duid) Modifier {
+func WithServerID(duid DUID) Modifier {
return WithOption(OptServerID(duid))
}
diff --git a/dhcpv6/modifiers_test.go b/dhcpv6/modifiers_test.go
index 1d48086..322d4e3 100644
--- a/dhcpv6/modifiers_test.go
+++ b/dhcpv6/modifiers_test.go
@@ -1,7 +1,6 @@
package dhcpv6
import (
- "log"
"net"
"testing"
"time"
@@ -11,27 +10,25 @@ import (
)
func TestWithClientID(t *testing.T) {
- duid := Duid{
- Type: DUID_LL,
- HwType: iana.HWTypeEthernet,
+ duid := &DUIDLL{
+ HWType: iana.HWTypeEthernet,
LinkLayerAddr: net.HardwareAddr([]byte{0xfa, 0xce, 0xb0, 0x00, 0x00, 0x0c}),
}
m, err := NewMessage(WithClientID(duid))
require.NoError(t, err)
cid := m.Options.ClientID()
- require.Equal(t, cid, &duid)
+ require.Equal(t, cid, duid)
}
func TestWithServerID(t *testing.T) {
- duid := Duid{
- Type: DUID_LL,
- HwType: iana.HWTypeEthernet,
+ duid := &DUIDLL{
+ HWType: iana.HWTypeEthernet,
LinkLayerAddr: net.HardwareAddr([]byte{0xfa, 0xce, 0xb0, 0x00, 0x00, 0x0c}),
}
m, err := NewMessage(WithServerID(duid))
require.NoError(t, err)
sid := m.Options.ServerID()
- require.Equal(t, sid, &duid)
+ require.Equal(t, sid, duid)
}
func TestWithRequestedOptions(t *testing.T) {
@@ -65,7 +62,6 @@ func TestWithDNS(t *testing.T) {
)(&d)
require.Equal(t, 1, len(d.Options.Options))
dns := d.Options.DNS()
- log.Printf("DNS %+v", dns)
require.Equal(t, 2, len(dns))
require.Equal(t, net.ParseIP("fe80::1"), dns[0])
require.Equal(t, net.ParseIP("fe80::2"), dns[1])
diff --git a/dhcpv6/option_4rd.go b/dhcpv6/option_4rd.go
index 13d672a..3b074f4 100644
--- a/dhcpv6/option_4rd.go
+++ b/dhcpv6/option_4rd.go
@@ -8,7 +8,9 @@ import (
)
// Opt4RD represents a 4RD option. It is only a container for 4RD_*_RULE options
-type Opt4RD Options
+type Opt4RD struct {
+ FourRDOptions
+}
// Code returns the Option Code for this option
func (op *Opt4RD) Code() OptionCode {
@@ -17,32 +19,75 @@ func (op *Opt4RD) Code() OptionCode {
// ToBytes serializes this option
func (op *Opt4RD) ToBytes() []byte {
- return (*Options)(op).ToBytes()
+ return op.Options.ToBytes()
}
// String returns a human-readable representation of the option
func (op *Opt4RD) String() string {
- return fmt.Sprintf("Opt4RD{%v}", (*Options)(op))
+ return fmt.Sprintf("%s: {Options=%v}", op.Code(), op.Options)
+}
+
+// LongString returns a multi-line human-readable representation of the option
+func (op *Opt4RD) LongString(indentSpace int) string {
+ return fmt.Sprintf("%s: Options=%v", op.Code(), op.Options.LongString(indentSpace))
}
-// ParseOpt4RD builds an Opt4RD structure from a sequence of bytes.
+// FromBytes builds an Opt4RD structure from a sequence of bytes.
// The input data does not include option code and length bytes
-func ParseOpt4RD(data []byte) (*Opt4RD, error) {
- var opt Options
- err := opt.FromBytes(data)
- return (*Opt4RD)(&opt), err
+func (op *Opt4RD) FromBytes(data []byte) error {
+ return op.Options.FromBytes(data)
+}
+
+// FourRDOptions are options that can be encapsulated with the 4RD option.
+type FourRDOptions struct {
+ Options
}
-// Opt4RDMapRule represents a 4RD Mapping Rule option
-// The option is described in https://tools.ietf.org/html/rfc7600#section-4.9
-// The 4RD mapping rules are described in https://tools.ietf.org/html/rfc7600#section-4.2
+// MapRules returns the map rules associated with the 4RD option.
+//
+// "The OPTION_4RD DHCPv6 option contains at least one encapsulated
+// OPTION_4RD_MAP_RULE option." (RFC 7600 Section 4.9)
+func (frdo FourRDOptions) MapRules() []*Opt4RDMapRule {
+ opts := frdo.Options.Get(Option4RDMapRule)
+ var mrs []*Opt4RDMapRule
+ for _, o := range opts {
+ if m, ok := o.(*Opt4RDMapRule); ok {
+ mrs = append(mrs, m)
+ }
+ }
+ return mrs
+}
+
+// NonMapRule returns the non-map-rule associated with this option.
+//
+// "The OPTION_4RD DHCPv6 option contains ... a maximum of one
+// encapsulated OPTION_4RD_NON_MAP_RULE option." (RFC 7600 Section 4.9)
+func (frdo FourRDOptions) NonMapRule() *Opt4RDNonMapRule {
+ opt := frdo.Options.GetOne(Option4RDNonMapRule)
+ if opt == nil {
+ return nil
+ }
+ nmr, ok := opt.(*Opt4RDNonMapRule)
+ if !ok {
+ return nil
+ }
+ return nmr
+}
+
+// Opt4RDMapRule represents a 4RD Mapping Rule option.
+//
+// The option is described in RFC 7600 Section 4.9. The 4RD mapping rules are
+// described in RFC 7600 Section 4.2.
type Opt4RDMapRule struct {
// Prefix4 is the IPv4 prefix mapped by this rule
Prefix4 net.IPNet
+
// Prefix6 is the IPv6 prefix mapped by this rule
Prefix6 net.IPNet
+
// EABitsLength is the number of bits of an address used in constructing the mapped address
EABitsLength uint8
+
// WKPAuthorized determines if well-known ports are assigned to addresses in an A+P mapping
// It can only be set if the length of Prefix4 + EABits > 32
WKPAuthorized bool
@@ -94,30 +139,31 @@ func (op *Opt4RDMapRule) ToBytes() []byte {
// String returns a human-readable description of this option
func (op *Opt4RDMapRule) String() string {
- return fmt.Sprintf("Opt4RDMapRule{Prefix4=%s, Prefix6=%s, EA-Bits=%d, WKPAuthorized=%t}",
- op.Prefix4.String(), op.Prefix6.String(), op.EABitsLength, op.WKPAuthorized)
+ return fmt.Sprintf("%s: {Prefix4=%s, Prefix6=%s, EA-Bits=%d, WKPAuthorized=%t}",
+ op.Code(), op.Prefix4.String(), op.Prefix6.String(), op.EABitsLength, op.WKPAuthorized)
}
-// ParseOpt4RDMapRule builds an Opt4RDMapRule structure from a sequence of bytes.
+// FromBytes builds an Opt4RDMapRule structure from a sequence of bytes.
// The input data does not include option code and length bytes.
-func ParseOpt4RDMapRule(data []byte) (*Opt4RDMapRule, error) {
- var opt Opt4RDMapRule
+func (op *Opt4RDMapRule) FromBytes(data []byte) error {
buf := uio.NewBigEndianBuffer(data)
- opt.Prefix4.Mask = net.CIDRMask(int(buf.Read8()), 32)
- opt.Prefix6.Mask = net.CIDRMask(int(buf.Read8()), 128)
- opt.EABitsLength = buf.Read8()
- opt.WKPAuthorized = (buf.Read8() & opt4RDWKPAuthorizedMask) != 0
- opt.Prefix4.IP = net.IP(buf.CopyN(net.IPv4len))
- opt.Prefix6.IP = net.IP(buf.CopyN(net.IPv6len))
- return &opt, buf.FinError()
+ op.Prefix4.Mask = net.CIDRMask(int(buf.Read8()), 32)
+ op.Prefix6.Mask = net.CIDRMask(int(buf.Read8()), 128)
+ op.EABitsLength = buf.Read8()
+ op.WKPAuthorized = (buf.Read8() & opt4RDWKPAuthorizedMask) != 0
+ op.Prefix4.IP = net.IP(buf.CopyN(net.IPv4len))
+ op.Prefix6.IP = net.IP(buf.CopyN(net.IPv6len))
+ return buf.FinError()
}
// Opt4RDNonMapRule represents 4RD parameters other than mapping rules
type Opt4RDNonMapRule struct {
// HubAndSpoke is whether the network topology is hub-and-spoke or meshed
HubAndSpoke bool
+
// TrafficClass is an optional 8-bit tunnel traffic class identifier
TrafficClass *uint8
+
// DomainPMTU is the Path MTU for this 4RD domain
DomainPMTU uint16
}
@@ -154,25 +200,23 @@ func (op *Opt4RDNonMapRule) String() string {
tClass = *op.TrafficClass
}
- return fmt.Sprintf("Opt4RDNonMapRule{HubAndSpoke=%t, TrafficClass=%v, DomainPMTU=%d}",
- op.HubAndSpoke, tClass, op.DomainPMTU)
+ return fmt.Sprintf("%s: {HubAndSpoke=%t, TrafficClass=%v, DomainPMTU=%d}", op.Code(), op.HubAndSpoke, tClass, op.DomainPMTU)
}
-// ParseOpt4RDNonMapRule builds an Opt4RDNonMapRule structure from a sequence of bytes.
+// FromBytes builds an Opt4RDNonMapRule structure from a sequence of bytes.
// The input data does not include option code and length bytes
-func ParseOpt4RDNonMapRule(data []byte) (*Opt4RDNonMapRule, error) {
- var opt Opt4RDNonMapRule
+func (op *Opt4RDNonMapRule) FromBytes(data []byte) error {
buf := uio.NewBigEndianBuffer(data)
flags := buf.Read8()
- opt.HubAndSpoke = flags&opt4RDHubAndSpokeMask != 0
+ op.HubAndSpoke = flags&opt4RDHubAndSpokeMask != 0
tClass := buf.Read8()
if flags&opt4RDTrafficClassMask != 0 {
- opt.TrafficClass = &tClass
+ op.TrafficClass = &tClass
}
- opt.DomainPMTU = buf.Read16()
+ op.DomainPMTU = buf.Read16()
- return &opt, buf.FinError()
+ return buf.FinError()
}
diff --git a/dhcpv6/option_4rd_test.go b/dhcpv6/option_4rd_test.go
index 1251b13..ca85bdd 100644
--- a/dhcpv6/option_4rd_test.go
+++ b/dhcpv6/option_4rd_test.go
@@ -1,46 +1,314 @@
package dhcpv6
import (
+ "bytes"
+ "errors"
+ "fmt"
"net"
+ "reflect"
"testing"
+ "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestOpt4RDNonMapRuleParse(t *testing.T) {
- data := []byte{0x81, 0xaa, 0x05, 0xd4}
- opt, err := ParseOpt4RDNonMapRule(data)
- require.NoError(t, err)
- require.True(t, opt.HubAndSpoke)
- require.NotNil(t, opt.TrafficClass)
- require.EqualValues(t, 0xaa, *opt.TrafficClass)
- require.EqualValues(t, 1492, opt.DomainPMTU)
+func Test4RDParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want []*Opt4RD
+ }{
+ {
+ buf: []byte{
+ 0, 97, // 4RD option code
+ 0, 28, // length
+ 0, 98, // 4RD Map Rule option
+ 0, 24, // length
+ 16, // prefix4-length
+ 16, // prefix6-length
+ 8, // ea-len
+ 0, // WKPAuthorized
+ 192, 168, 0, 1, // rule-ipv4-prefix
+ 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // rule-ipv6-prefix
+ },
+ want: []*Opt4RD{
+ &Opt4RD{
+ FourRDOptions: FourRDOptions{Options: Options{
+ &Opt4RDMapRule{
+ Prefix4: net.IPNet{
+ IP: net.IP{192, 168, 0, 1},
+ Mask: net.CIDRMask(16, 32),
+ },
+ Prefix6: net.IPNet{
+ IP: net.ParseIP("fe80::"),
+ Mask: net.CIDRMask(16, 128),
+ },
+ EABitsLength: 8,
+ },
+ }},
+ },
+ },
+ },
+ {
+ buf: []byte{
+ 0, 97, // 4RD option code
+ 0, 28, // length
+ 0, 98, // 4RD Map Rule option
+ 0, 24, // length
+ 16, // prefix4-length
+ 16, // prefix6-length
+ 8, // ea-len
+ 0, // WKPAuthorized
+ 192, 168, 0, 1, // rule-ipv4-prefix
+ 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // rule-ipv6-prefix
- // Remove the TrafficClass flag and check value is ignored
- data[0] = 0x80
- opt, err = ParseOpt4RDNonMapRule(data)
- require.NoError(t, err)
- require.True(t, opt.HubAndSpoke)
- require.Nil(t, opt.TrafficClass)
- require.EqualValues(t, 1492, opt.DomainPMTU)
+ 0, 97, // 4RD
+ 0, 8, // length
+ 0, 99, // 4RD non map rule
+ 0, 4, // length
+ 0x80, 0x00, 0x05, 0xd4,
+ },
+ want: []*Opt4RD{
+ &Opt4RD{
+ FourRDOptions: FourRDOptions{Options: Options{
+ &Opt4RDMapRule{
+ Prefix4: net.IPNet{
+ IP: net.IP{192, 168, 0, 1},
+ Mask: net.CIDRMask(16, 32),
+ },
+ Prefix6: net.IPNet{
+ IP: net.ParseIP("fe80::"),
+ Mask: net.CIDRMask(16, 128),
+ },
+ EABitsLength: 8,
+ },
+ }},
+ },
+ &Opt4RD{
+ FourRDOptions: FourRDOptions{Options: Options{
+ &Opt4RDNonMapRule{
+ HubAndSpoke: true,
+ DomainPMTU: 1492,
+ },
+ }},
+ },
+ },
+ },
+ {
+ buf: []byte{0, 97, 0, 1, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ {
+ // Allowed, because the RFC doesn't really specify that
+ // it can't be empty. RFC doesn't really specify
+ // anything, frustratingly.
+ buf: []byte{
+ 0, 97, // 4RD option code
+ 0, 0, // length
+ },
+ want: []*Opt4RD{&Opt4RD{FourRDOptions: FourRDOptions{Options: Options{}}}},
+ },
+ {
+ buf: []byte{
+ 0, 97, // 4RD option code
+ 0, 6, // length
+ 0, 98, // 4RD Map Rule option
+ 0, 4, // length
+ 16, // prefix4-length
+ 16, // prefix6-length
+ 8, // ea-len
+ 0, // WKPAuthorized
+ // Missing
+ },
+ want: nil,
+ err: uio.ErrBufferTooShort,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.FourRD(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("FourRD = %v, want %v", got, tt.want)
+ }
+ if len(tt.want) >= 1 {
+ var b MessageOptions
+ for _, frd := range tt.want {
+ b.Add(frd)
+ }
+ got := b.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
+ }
}
-func TestOpt4RDNonMapRuleToBytes(t *testing.T) {
- var tClass uint8 = 0xaa
- opt := Opt4RDNonMapRule{
- HubAndSpoke: true,
- TrafficClass: &tClass,
- DomainPMTU: 1492,
+func Test4RDMapRuleParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want []*Opt4RDMapRule
+ }{
+ {
+ buf: []byte{
+ 0, 98, // 4RD Map Rule option
+ 0, 24, // length
+ 16, // prefix4-length
+ 16, // prefix6-length
+ 8, // ea-len
+ 0, // WKPAuthorized
+ 192, 168, 0, 1, // rule-ipv4-prefix
+ 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // rule-ipv6-prefix
+ },
+ want: []*Opt4RDMapRule{
+ &Opt4RDMapRule{
+ Prefix4: net.IPNet{
+ IP: net.IP{192, 168, 0, 1},
+ Mask: net.CIDRMask(16, 32),
+ },
+ Prefix6: net.IPNet{
+ IP: net.ParseIP("fe80::"),
+ Mask: net.CIDRMask(16, 128),
+ },
+ EABitsLength: 8,
+ },
+ },
+ },
+ {
+ buf: []byte{
+ 0, 98, // 4RD Map Rule option
+ 0, 24, // length
+ 16, // prefix4-length
+ 16, // prefix6-length
+ 8, // ea-len
+ 1 << 7, // WKPAuthorized
+ 192, 168, 0, 1, // rule-ipv4-prefix
+ 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // rule-ipv6-prefix
+ },
+ want: []*Opt4RDMapRule{
+ &Opt4RDMapRule{
+ Prefix4: net.IPNet{
+ IP: net.IP{192, 168, 0, 1},
+ Mask: net.CIDRMask(16, 32),
+ },
+ Prefix6: net.IPNet{
+ IP: net.ParseIP("fe80::"),
+ Mask: net.CIDRMask(16, 128),
+ },
+ EABitsLength: 8,
+ WKPAuthorized: true,
+ },
+ },
+ },
+ {
+ buf: []byte{0, 98, 0, 1, 0},
+ want: nil,
+ err: uio.ErrBufferTooShort,
+ },
+ {
+ buf: []byte{
+ 0, 98, // 4RD Map Rule option
+ 0, 4, // length
+ 16, // prefix4-length
+ 16, // prefix6-length
+ 8, // ea-len
+ 0, // WKPAuthorized
+ // Missing
+ },
+ want: nil,
+ err: uio.ErrBufferTooShort,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var frdo FourRDOptions
+ if err := frdo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := frdo.MapRules(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("MapRules = %v, want %v", got, tt.want)
+ }
+ if len(tt.want) >= 1 {
+ var b FourRDOptions
+ for _, frd := range tt.want {
+ b.Add(frd)
+ }
+ got := b.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
}
- expected := []byte{0x81, 0xaa, 0x05, 0xd4}
-
- require.Equal(t, expected, opt.ToBytes())
-
- // Unsetting TrafficClass should zero the corresponding bytes in the output
- opt.TrafficClass = nil
- expected[0], expected[1] = 0x80, 0x00
+}
- require.Equal(t, expected, opt.ToBytes())
+func Test4RDNonMapRuleParseAndGetter(t *testing.T) {
+ trafficClassOne := uint8(1)
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want *Opt4RDNonMapRule
+ }{
+ {
+ buf: []byte{
+ 0, 99, // 4RD Non Map Rule option
+ 0, 4, // length
+ 0x80, 0, 0x05, 0xd4,
+ },
+ want: &Opt4RDNonMapRule{
+ HubAndSpoke: true,
+ DomainPMTU: 1492,
+ },
+ },
+ {
+ buf: []byte{
+ 0, 99, // 4RD Non Map Rule option
+ 0, 4, // length
+ 0, 0, 0x05, 0xd4,
+ },
+ want: &Opt4RDNonMapRule{
+ DomainPMTU: 1492,
+ },
+ },
+ {
+ buf: []byte{
+ 0, 99, // 4RD Non Map Rule option
+ 0, 4, // length
+ 0x1, 0x01, 0x05, 0xd4,
+ },
+ want: &Opt4RDNonMapRule{
+ TrafficClass: &trafficClassOne,
+ DomainPMTU: 1492,
+ },
+ },
+ {
+ buf: []byte{0, 99, 0, 1, 0},
+ want: nil,
+ err: uio.ErrBufferTooShort,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var frdo FourRDOptions
+ if err := frdo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := frdo.NonMapRule(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("NonMapRule = %v, want %v", got, tt.want)
+ }
+ if tt.want != nil {
+ var b FourRDOptions
+ b.Add(tt.want)
+ got := b.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
+ }
}
func TestOpt4RDNonMapRuleString(t *testing.T) {
@@ -61,58 +329,21 @@ func TestOpt4RDNonMapRuleString(t *testing.T) {
"String() should contain the domain PMTU")
}
-func TestOpt4RDMapRuleParse(t *testing.T) {
- ip6addr, ip6net, err := net.ParseCIDR("2001:db8::1234:5678:0:aabb/64")
- ip6net.IP = ip6addr // We want to keep the entire address however, not apply the mask
- require.NoError(t, err)
- ip4addr, ip4net, err := net.ParseCIDR("100.64.0.234/10")
- ip4net.IP = ip4addr.To4()
- require.NoError(t, err)
- data := append([]byte{
- 10, // IPv4 prefix length
- 64, // IPv6 prefix length
- 32, // EA-bits
- 0x80, // WKPs authorized
- },
- append(ip4addr.To4(), ip6addr...)...,
- )
-
- opt, err := ParseOpt4RDMapRule(data)
- require.NoError(t, err)
- require.EqualValues(t, *ip6net, opt.Prefix6)
- require.EqualValues(t, *ip4net, opt.Prefix4)
- require.EqualValues(t, 32, opt.EABitsLength)
- require.True(t, opt.WKPAuthorized)
-}
-
func TestOpt4RDMapRuleToBytes(t *testing.T) {
opt := Opt4RDMapRule{
- Prefix4: net.IPNet{
- IP: net.IPv4(100, 64, 0, 238),
- Mask: net.CIDRMask(24, 32),
- },
- Prefix6: net.IPNet{
- IP: net.ParseIP("2001:db8::1234:5678:0:aabb"),
- Mask: net.CIDRMask(80, 128),
- },
EABitsLength: 32,
WKPAuthorized: true,
}
expected := append([]byte{
- 24, // v4 prefix length
- 80, // v6 prefix length
+ 0, // v4 prefix length
+ 0, // v6 prefix length
32, // EA-bits
0x80, // WKPs authorized
- },
- append(opt.Prefix4.IP.To4(), opt.Prefix6.IP.To16()...)...,
- )
-
+ }, bytes.Repeat([]byte{0x00}, 4+16)...)
require.Equal(t, expected, opt.ToBytes())
}
-// FIXME: Invalid packets are serialized without error
-
func TestOpt4RDMapRuleString(t *testing.T) {
opt := Opt4RDMapRule{
Prefix4: net.IPNet{
@@ -135,34 +366,3 @@ func TestOpt4RDMapRuleString(t *testing.T) {
"String() should include the IPv4 prefix")
require.Contains(t, str, "EA-Bits=32", "String() should include the value for EA-Bits")
}
-
-// This test round-trip serialization/deserialization of both kinds of 4RD
-// options, and the container option
-func TestOpt4RDRoundTrip(t *testing.T) {
- var tClass uint8 = 0xaa
- opt := Opt4RD{
- &Opt4RDMapRule{
- Prefix4: net.IPNet{
- IP: net.IPv4(100, 64, 0, 238).To4(),
- Mask: net.CIDRMask(24, 32),
- },
- Prefix6: net.IPNet{
- IP: net.ParseIP("2001:db8::1234:5678:0:aabb"),
- Mask: net.CIDRMask(80, 128),
- },
- EABitsLength: 32,
- WKPAuthorized: true,
- },
- &Opt4RDNonMapRule{
- HubAndSpoke: true,
- TrafficClass: &tClass,
- DomainPMTU: 9000,
- },
- }
-
- rtOpt, err := ParseOpt4RD(opt.ToBytes())
-
- require.NoError(t, err)
- require.NotNil(t, rtOpt)
- require.Equal(t, opt, *rtOpt)
-}
diff --git a/dhcpv6/option_archtype.go b/dhcpv6/option_archtype.go
index 1461056..2b778d8 100644
--- a/dhcpv6/option_archtype.go
+++ b/dhcpv6/option_archtype.go
@@ -23,13 +23,9 @@ func (op *optClientArchType) Code() OptionCode {
}
func (op optClientArchType) String() string {
- return fmt.Sprintf("ClientArchType: %s", op.Archs.String())
+ return fmt.Sprintf("%s: %s", op.Code(), op.Archs)
}
-// parseOptClientArchType builds an OptClientArchType structure from
-// a sequence of bytes The input data does not include option code and
-// length bytes.
-func parseOptClientArchType(data []byte) (*optClientArchType, error) {
- var opt optClientArchType
- return &opt, opt.FromBytes(data)
+func (op *optClientArchType) FromBytes(p []byte) error {
+ return op.Archs.FromBytes(p)
}
diff --git a/dhcpv6/option_archtype_test.go b/dhcpv6/option_archtype_test.go
index 0481c1a..b8a39fd 100644
--- a/dhcpv6/option_archtype_test.go
+++ b/dhcpv6/option_archtype_test.go
@@ -1,34 +1,74 @@
package dhcpv6
import (
+ "errors"
+ "fmt"
+ "reflect"
"testing"
+ "github.com/google/go-cmp/cmp"
"github.com/insomniacslk/dhcp/iana"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestParseOptClientArchType(t *testing.T) {
- data := []byte{
- 0, 6, // EFI_IA32
- }
- opt, err := parseOptClientArchType(data)
- require.NoError(t, err)
- require.Equal(t, iana.EFI_IA32, opt.Archs[0])
-}
-
-func TestParseOptClientArchTypeInvalid(t *testing.T) {
- data := []byte{42}
- _, err := parseOptClientArchType(data)
- require.Error(t, err)
-}
+func TestArchTypeParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want iana.Archs
+ }{
+ {
+ buf: []byte{
+ 0, 61, // Client Arch Types option
+ 0, 2, // length
+ 0, 7, // EFI_X86_64
+ },
+ want: iana.Archs{iana.EFI_X86_64},
+ },
+ {
+ buf: []byte{
+ 0, 61, // Client Arch Types option
+ 0, 4, // length
+ 0, 7, // EFI_X86_64
+ 0, 8, // EFI_XSCALE
+ },
+ want: iana.Archs{iana.EFI_X86_64, iana.EFI_XSCALE},
+ },
+ {
+ buf: nil,
+ want: nil,
+ },
+ {
+ buf: []byte{0, 61, 0, 1, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ {
+ buf: []byte{0, 61, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.ArchTypes(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("ArchTypes = %v, want %v", got, tt.want)
+ }
-func TestOptClientArchTypeParseAndToBytes(t *testing.T) {
- data := []byte{
- 0, 8, // EFI_XSCALE
+ if tt.want != nil {
+ var m MessageOptions
+ m.Add(OptClientArchType(tt.want...))
+ got := m.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
}
- opt, err := parseOptClientArchType(data)
- require.NoError(t, err)
- require.Equal(t, data, opt.ToBytes())
}
func TestOptClientArchType(t *testing.T) {
diff --git a/dhcpv6/option_bootfileparam.go b/dhcpv6/option_bootfileparam.go
index 1bd54bc..ba09ca0 100644
--- a/dhcpv6/option_bootfileparam.go
+++ b/dhcpv6/option_bootfileparam.go
@@ -9,10 +9,12 @@ import (
// OptBootFileParam returns a BootfileParam option as defined in RFC 5970
// Section 3.2.
func OptBootFileParam(args ...string) Option {
- return optBootFileParam(args)
+ return &optBootFileParam{args}
}
-type optBootFileParam []string
+type optBootFileParam struct {
+ params []string
+}
// Code returns the option code
func (optBootFileParam) Code() OptionCode {
@@ -22,7 +24,7 @@ func (optBootFileParam) Code() OptionCode {
// ToBytes serializes the option and returns it as a sequence of bytes
func (op optBootFileParam) ToBytes() []byte {
buf := uio.NewBigEndianBuffer(nil)
- for _, param := range op {
+ for _, param := range op.params {
if len(param) >= 1<<16 {
// TODO: say something here instead of silently ignoring a parameter
continue
@@ -42,20 +44,16 @@ func (op optBootFileParam) ToBytes() []byte {
}
func (op optBootFileParam) String() string {
- return fmt.Sprintf("BootFileParam: %v", ([]string)(op))
+ return fmt.Sprintf("%s: %v", op.Code(), op.params)
}
-// parseOptBootFileParam builds an OptBootFileParam structure from a sequence
+// FromBytes builds an OptBootFileParam structure from a sequence
// of bytes. The input data does not include option code and length bytes.
-func parseOptBootFileParam(data []byte) (optBootFileParam, error) {
+func (op *optBootFileParam) FromBytes(data []byte) error {
buf := uio.NewBigEndianBuffer(data)
- var result optBootFileParam
for buf.Has(2) {
length := buf.Read16()
- result = append(result, string(buf.CopyN(int(length))))
- }
- if err := buf.FinError(); err != nil {
- return nil, err
+ op.params = append(op.params, string(buf.CopyN(int(length))))
}
- return result, nil
+ return buf.FinError()
}
diff --git a/dhcpv6/option_bootfileparam_test.go b/dhcpv6/option_bootfileparam_test.go
index 467f245..d2e92a7 100644
--- a/dhcpv6/option_bootfileparam_test.go
+++ b/dhcpv6/option_bootfileparam_test.go
@@ -2,70 +2,78 @@ package dhcpv6
import (
"bytes"
- "encoding/binary"
+ "errors"
"fmt"
+ "reflect"
+ "strings"
"testing"
- "github.com/stretchr/testify/require"
+ "github.com/google/go-cmp/cmp"
+ "github.com/u-root/uio/uio"
)
-var (
- testBootfileParams0Compiled = "\x00\x0eroot=/dev/sda1\x00\x00\x00\x02rw"
- testBootfileParams1 = []string{
- "initrd=http://myserver.mycompany.local/initrd.xz",
- "",
- "root=/dev/sda1",
- "rw",
- "netconsole=..:\000:.something\000here.::..",
- string(make([]byte, (1<<16)-1)),
+func TestBootFileParamLargeParameter(t *testing.T) {
+ param := []string{
+ "foo=bar",
+ strings.Repeat("a", 1<<16),
}
-)
+ var m MessageOptions
+ m.Add(OptBootFileParam(param...))
+ want := append([]byte{
+ 0, 60, // Boot File Param
+ 0, 9, // length
+ 0, 7,
+ }, []byte("foo=bar")...)
-// compileTestBootfileParams is an independent implementation of bootfile param encoder
-func compileTestBootfileParams(t *testing.T, params []string) []byte {
- var length [2]byte
- buf := bytes.Buffer{}
- for _, param := range params {
- if len(param) >= 1<<16 {
- panic("a too long parameter")
- }
- binary.BigEndian.PutUint16(length[:], uint16(len(param)))
- _, err := buf.Write(length[:])
- require.NoError(t, err)
- _, err = buf.WriteString(param)
- require.NoError(t, err)
+ got := m.ToBytes()
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
}
-
- return buf.Bytes()
}
-func TestOptBootFileParam(t *testing.T) {
- expected := string(compileTestBootfileParams(t, testBootfileParams1))
- opt, err := parseOptBootFileParam([]byte(expected))
- if err != nil {
- t.Fatal(err)
- }
- if string(opt.ToBytes()) != expected {
- t.Fatalf("Invalid boot file parameter. Expected %v, got %v", expected, opt)
- }
+func joinBytes(p ...[]byte) []byte {
+ return bytes.Join(p, nil)
}
-func TestParsedTypeOptBootFileParam(t *testing.T) {
- tryParse := func(compiled []byte, expected []string) {
- opt, err := ParseOption(OptionBootfileParam, compiled)
- require.NoError(t, err)
- bootfileParamOpt, ok := opt.(optBootFileParam)
- require.True(t, ok, fmt.Sprintf("invalid type: %T instead of %T", opt, bootfileParamOpt))
- require.Equal(t, compiled, bootfileParamOpt.ToBytes())
- require.Equal(t, expected, ([]string)(bootfileParamOpt))
- }
+func TestBootFileParamParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want []string
+ }{
+ {
+ buf: joinBytes([]byte{
+ 0, 60, // Boot File Param
+ 0, 25, // length
+ 0, 14,
+ }, []byte("root=/dev/sda1"), []byte{0, 7}, []byte("foo=bar")),
+ want: []string{"root=/dev/sda1", "foo=bar"},
+ },
+ {
+ buf: nil,
+ },
+ {
+ buf: []byte{0, 60, 0},
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.BootFileParam(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("BootFileParam = %v, want %v", got, tt.want)
+ }
- tryParse(
- []byte(testBootfileParams0Compiled),
- []string{"root=/dev/sda1", "", "rw"},
- )
- tryParse(
- compileTestBootfileParams(t, testBootfileParams1),
- testBootfileParams1,
- )
+ if tt.want != nil {
+ var m MessageOptions
+ m.Add(OptBootFileParam(tt.want...))
+ got := m.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
+ }
}
diff --git a/dhcpv6/option_bootfileurl.go b/dhcpv6/option_bootfileurl.go
index 695f164..7a8e54a 100644
--- a/dhcpv6/option_bootfileurl.go
+++ b/dhcpv6/option_bootfileurl.go
@@ -6,10 +6,12 @@ import (
// OptBootFileURL returns a OptionBootfileURL as defined by RFC 5970.
func OptBootFileURL(url string) Option {
- return optBootFileURL(url)
+ return &optBootFileURL{url}
}
-type optBootFileURL string
+type optBootFileURL struct {
+ url string
+}
// Code returns the option code
func (op optBootFileURL) Code() OptionCode {
@@ -18,15 +20,16 @@ func (op optBootFileURL) Code() OptionCode {
// ToBytes serializes the option and returns it as a sequence of bytes
func (op optBootFileURL) ToBytes() []byte {
- return []byte(op)
+ return []byte(op.url)
}
func (op optBootFileURL) String() string {
- return fmt.Sprintf("BootFileURL: %s", string(op))
+ return fmt.Sprintf("%s: %s", op.Code(), op.url)
}
-// parseOptBootFileURL builds an optBootFileURL structure from a sequence
+// FromBytes builds an optBootFileURL structure from a sequence
// of bytes. The input data does not include option code and length bytes.
-func parseOptBootFileURL(data []byte) (optBootFileURL, error) {
- return optBootFileURL(string(data)), nil
+func (op *optBootFileURL) FromBytes(data []byte) error {
+ op.url = string(data)
+ return nil
}
diff --git a/dhcpv6/option_bootfileurl_test.go b/dhcpv6/option_bootfileurl_test.go
index ac45ef5..726fde8 100644
--- a/dhcpv6/option_bootfileurl_test.go
+++ b/dhcpv6/option_bootfileurl_test.go
@@ -1,29 +1,59 @@
package dhcpv6
import (
- "bytes"
+ "errors"
+ "fmt"
"testing"
+ "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestOptBootFileURL(t *testing.T) {
- expected := "https://insomniac.slackware.it"
- opt, err := parseOptBootFileURL([]byte(expected))
- if err != nil {
- t.Fatal(err)
- }
- if string(opt) != expected {
- t.Fatalf("Invalid boot file URL. Expected %v, got %v", expected, opt)
+func TestBootFileURLParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want string
+ }{
+ {
+ buf: []byte{
+ 0, 59, // Boot File URL
+ 0, 17, // length
+ 'h', 't', 't', 'p', ':', '/', '/', 'u', '-', 'r', 'o', 'o', 't', '.', 'o', 'r', 'g',
+ },
+ want: "http://u-root.org",
+ },
+ {
+ buf: nil,
+ },
+ {
+ buf: []byte{0, 59, 0},
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.BootFileURL(); got != tt.want {
+ t.Errorf("BootFileURL = %v, want %v", got, tt.want)
+ }
+
+ if tt.want != "" {
+ var m MessageOptions
+ m.Add(OptBootFileURL(tt.want))
+ got := m.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
}
- require.Contains(t, opt.String(), "https://insomniac.slackware.it", "String() should contain the correct BootFileUrl output")
}
-func TestOptBootFileURLToBytes(t *testing.T) {
- urlString := "https://insomniac.slackware.it"
- opt := OptBootFileURL(urlString)
- toBytes := opt.ToBytes()
- if !bytes.Equal(toBytes, []byte(urlString)) {
- t.Fatalf("Invalid ToBytes result. Expected %v, got %v", urlString, toBytes)
- }
+func TestOptBootFileURL(t *testing.T) {
+ opt := OptBootFileURL("https://insomniac.slackware.it")
+ require.Contains(t, opt.String(), "https://insomniac.slackware.it", "String() should contain the correct BootFileUrl output")
}
diff --git a/dhcpv6/option_clientid.go b/dhcpv6/option_clientid.go
index 0941bcc..eea0d01 100644
--- a/dhcpv6/option_clientid.go
+++ b/dhcpv6/option_clientid.go
@@ -6,12 +6,12 @@ import (
// OptClientID represents a Client Identifier option as defined by RFC 3315
// Section 22.2.
-func OptClientID(d Duid) Option {
+func OptClientID(d DUID) Option {
return &optClientID{d}
}
type optClientID struct {
- Duid
+ DUID
}
func (*optClientID) Code() OptionCode {
@@ -19,16 +19,14 @@ func (*optClientID) Code() OptionCode {
}
func (op *optClientID) String() string {
- return fmt.Sprintf("ClientID: %v", op.Duid.String())
+ return fmt.Sprintf("%s: %s", op.Code(), op.DUID)
}
-// parseOptClientID builds an OptClientId structure from a sequence
+// FromBytes builds an optClientID structure from a sequence
// of bytes. The input data does not include option code and length
// bytes.
-func parseOptClientID(data []byte) (*optClientID, error) {
- cid, err := DuidFromBytes(data)
- if err != nil {
- return nil, err
- }
- return &optClientID{*cid}, nil
+func (op *optClientID) FromBytes(data []byte) error {
+ var err error
+ op.DUID, err = DUIDFromBytes(data)
+ return err
}
diff --git a/dhcpv6/option_clientid_test.go b/dhcpv6/option_clientid_test.go
index 2f1f1f0..a80403a 100644
--- a/dhcpv6/option_clientid_test.go
+++ b/dhcpv6/option_clientid_test.go
@@ -1,58 +1,111 @@
package dhcpv6
import (
+ "errors"
+ "fmt"
"net"
+ "reflect"
"testing"
+ "github.com/google/go-cmp/cmp"
"github.com/insomniacslk/dhcp/iana"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestParseOptClientID(t *testing.T) {
- data := []byte{
- 0, 3, // DUID_LL
- 0, 1, // hwtype ethernet
- 0, 1, 2, 3, 4, 5, // hw addr
+func TestClientIDParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want DUID
+ }{
+ {
+ buf: []byte{
+ 0, 1, // Client ID option
+ 0, 10, // length
+ 0, 3, // DUID_LL
+ 0, 1, // hwtype ethernet
+ 0, 1, 2, 3, 4, 5, // HW addr
+ },
+ want: &DUIDLL{HWType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr{0, 1, 2, 3, 4, 5}},
+ },
+ {
+ buf: nil,
+ want: nil,
+ },
+ {
+ buf: []byte{0, 1, 0, 1, 0},
+ want: nil,
+ err: uio.ErrBufferTooShort,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.ClientID(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("ClientID = %v, want %v", got, tt.want)
+ }
+ })
}
- opt, err := parseOptClientID(data)
- require.NoError(t, err)
- require.Equal(t, DUID_LL, opt.Type)
- require.Equal(t, iana.HWTypeEthernet, opt.HwType)
- require.Equal(t, net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5}), opt.LinkLayerAddr)
}
-func TestOptClientIdToBytes(t *testing.T) {
- opt := OptClientID(
- Duid{
- Type: DUID_LL,
- HwType: iana.HWTypeEthernet,
- LinkLayerAddr: net.HardwareAddr([]byte{5, 4, 3, 2, 1, 0}),
+func TestClientID(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ want optClientID
+ err error
+ }{
+ {
+ buf: []byte{
+ 0, 3, // DUID_LL
+ 0, 1, // hwtype ethernet
+ 0, 1, 2, 3, 4, 5, // hw addr
+ },
+ want: optClientID{
+ &DUIDLL{
+ HWType: iana.HWTypeEthernet,
+ LinkLayerAddr: net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5}),
+ },
+ },
},
- )
- expected := []byte{
- 0, 3, // DUID_LL
- 0, 1, // hwtype ethernet
- 5, 4, 3, 2, 1, 0, // hw addr
- }
- require.Equal(t, expected, opt.ToBytes())
-}
+ {
+ buf: []byte{0},
+ err: uio.ErrBufferTooShort,
+ },
+ {
+ buf: []byte{0, 3, 0},
+ err: uio.ErrBufferTooShort,
+ },
+ {
+ buf: nil,
+ err: uio.ErrBufferTooShort,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var opt optClientID
+ if err := opt.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if tt.err == nil {
+ if !reflect.DeepEqual(tt.want, opt) {
+ t.Errorf("FromBytes = %v, want %v", opt, tt.want)
+ }
-func TestOptClientIdDecodeEncode(t *testing.T) {
- data := []byte{
- 0, 3, // DUID_LL
- 0, 1, // hwtype ethernet
- 5, 4, 3, 2, 1, 0, // hw addr
+ out := tt.want.ToBytes()
+ if diff := cmp.Diff(tt.buf, out); diff != "" {
+ t.Errorf("ToBytes mismatch: (-want, +got):\n%s", diff)
+ }
+ }
+ })
}
- opt, err := parseOptClientID(data)
- require.NoError(t, err)
- require.Equal(t, data, opt.ToBytes())
}
-func TestOptionClientId(t *testing.T) {
+func TestOptionClientIDString(t *testing.T) {
opt := OptClientID(
- Duid{
- Type: DUID_LL,
- HwType: iana.HWTypeEthernet,
+ &DUIDLL{
+ HWType: iana.HWTypeEthernet,
LinkLayerAddr: net.HardwareAddr([]byte{0xde, 0xad, 0, 0, 0xbe, 0xef}),
},
)
@@ -60,25 +113,7 @@ func TestOptionClientId(t *testing.T) {
require.Contains(
t,
opt.String(),
- "ClientID: DUID{type=DUID-LL hwtype=Ethernet hwaddr=de:ad:00:00:be:ef}",
+ "Client ID: DUID-LL{HWType=Ethernet HWAddr=de:ad:00:00:be:ef}",
"String() should contain the correct cid output",
)
}
-
-func TestOptClientIdparseOptClientIDBogusDUID(t *testing.T) {
- data := []byte{
- 0, 4, // DUID_UUID
- 1, 2, 3, 4, 5, 6, 7, 8, 9, // a UUID should be 18 bytes not 17
- 10, 11, 12, 13, 14, 15, 16, 17,
- }
- _, err := parseOptClientID(data)
- require.Error(t, err, "A truncated OptClientId DUID should return an error")
-}
-
-func TestOptClientIdparseOptClientIDInvalidTooShort(t *testing.T) {
- data := []byte{
- 0, // truncated: DUIDs are at least 2 bytes
- }
- _, err := parseOptClientID(data)
- require.Error(t, err, "A truncated OptClientId should return an error")
-}
diff --git a/dhcpv6/option_clientlinklayeraddress.go b/dhcpv6/option_clientlinklayeraddress.go
index 7fe5bee..878a576 100644
--- a/dhcpv6/option_clientlinklayeraddress.go
+++ b/dhcpv6/option_clientlinklayeraddress.go
@@ -33,15 +33,14 @@ func (op *optClientLinkLayerAddress) ToBytes() []byte {
}
func (op *optClientLinkLayerAddress) String() string {
- return fmt.Sprintf("ClientLinkLayerAddress: Type=%s LinkLayerAddress=%s", op.LinkLayerType, op.LinkLayerAddress)
+ return fmt.Sprintf("%s: Type=%s LinkLayerAddress=%s", op.Code(), op.LinkLayerType, op.LinkLayerAddress)
}
-// parseOptClientLinkLayerAddress deserializes from bytes
-// to build an optClientLinkLayerAddress structure.
-func parseOptClientLinkLayerAddress(data []byte) (*optClientLinkLayerAddress, error) {
- var opt optClientLinkLayerAddress
+// FromBytes deserializes from bytes to build an optClientLinkLayerAddress
+// structure.
+func (op *optClientLinkLayerAddress) FromBytes(data []byte) error {
buf := uio.NewBigEndianBuffer(data)
- opt.LinkLayerType = iana.HWType(buf.Read16())
- opt.LinkLayerAddress = buf.ReadAll()
- return &opt, buf.FinError()
+ op.LinkLayerType = iana.HWType(buf.Read16())
+ op.LinkLayerAddress = buf.ReadAll()
+ return buf.FinError()
}
diff --git a/dhcpv6/option_clientlinklayeraddress_test.go b/dhcpv6/option_clientlinklayeraddress_test.go
index a25e882..bd603e7 100644
--- a/dhcpv6/option_clientlinklayeraddress_test.go
+++ b/dhcpv6/option_clientlinklayeraddress_test.go
@@ -2,39 +2,65 @@ package dhcpv6
import (
"bytes"
+ "errors"
+ "fmt"
"net"
"testing"
+ "github.com/google/go-cmp/cmp"
"github.com/insomniacslk/dhcp/iana"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestParseOptClientLinkLayerAddress(t *testing.T) {
- data := []byte{
- 0, 1, // LinkLayerType
- 164, 131, 231, 227, 223, 136,
- }
- opt, err := parseOptClientLinkLayerAddress(data)
+func TestClientLinkLayerAddressParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ wantHWType iana.HWType
+ wantHWAddr net.HardwareAddr
+ }{
+ {
+ buf: []byte{
+ 0, 79, // Client Link Layer Address option
+ 0, 8, // length
+ 0, 1, // Ethernet
+ 1, 2, 3, 4, 5, 6,
+ },
+ wantHWType: iana.HWTypeEthernet,
+ wantHWAddr: net.HardwareAddr{1, 2, 3, 4, 5, 6},
+ },
+ {
+ buf: []byte{0, 79, 0, 1, 0},
+ err: uio.ErrBufferTooShort,
+ },
+ {
+ buf: []byte{0, 79, 0},
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var ro RelayOptions
+ if err := ro.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if gotHWType, gotHWAddr := ro.ClientLinkLayerAddress(); gotHWType != tt.wantHWType || !bytes.Equal(gotHWAddr, tt.wantHWAddr) {
+ t.Errorf("ClientLinkLayerAddress = (%s, %v), want (%s, %v)", gotHWType, tt.wantHWType, gotHWAddr, tt.wantHWAddr)
+ }
- require.NoError(t, err)
- require.Equal(t, OptionClientLinkLayerAddr, opt.Code())
- require.Equal(t, iana.HWTypeEthernet, opt.LinkLayerType)
- require.Equal(t, net.HardwareAddr(data[2:]), opt.LinkLayerAddress)
- require.Equal(t, "ClientLinkLayerAddress: Type=Ethernet LinkLayerAddress=a4:83:e7:e3:df:88", opt.String())
+ if tt.err == nil {
+ var m MessageOptions
+ m.Add(OptClientLinkLayerAddress(tt.wantHWType, tt.wantHWAddr))
+ got := m.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
+ }
}
-func TestOptClientLinkLayerAddressToBytes(t *testing.T) {
- mac, _ := net.ParseMAC("a4:83:e7:e3:df:88")
- opt := optClientLinkLayerAddress{
- LinkLayerType: iana.HWTypeEthernet,
- LinkLayerAddress: mac,
- }
- want := []byte{
- 0, 1, // LinkLayerType
- 164, 131, 231, 227, 223, 136,
- }
- b := opt.ToBytes()
- if !bytes.Equal(b, want) {
- t.Fatalf("opt.ToBytes()=%v, want %v", b, want)
- }
+func TestOptClientLinkLayerAddressString(t *testing.T) {
+ opt := OptClientLinkLayerAddress(iana.HWTypeEthernet, net.HardwareAddr{0xa4, 0x83, 0xe7, 0xe3, 0xdf, 0x88})
+ require.Equal(t, "Client Link-Layer Address: Type=Ethernet LinkLayerAddress=a4:83:e7:e3:df:88", opt.String())
}
diff --git a/dhcpv6/option_dhcpv4_msg.go b/dhcpv6/option_dhcpv4_msg.go
index 0a1a2b3..825ea8a 100644
--- a/dhcpv6/option_dhcpv4_msg.go
+++ b/dhcpv6/option_dhcpv4_msg.go
@@ -2,6 +2,7 @@ package dhcpv6
import (
"fmt"
+ "strings"
"github.com/insomniacslk/dhcp/dhcpv4"
)
@@ -25,15 +26,24 @@ func (op *OptDHCPv4Msg) ToBytes() []byte {
}
func (op *OptDHCPv4Msg) String() string {
- return fmt.Sprintf("OptDHCPv4Msg{%v}", op.Msg)
+ return fmt.Sprintf("%s: %v", op.Code(), op.Msg)
}
-// ParseOptDHCPv4Msg builds an OptDHCPv4Msg structure
-// from a sequence of bytes. The input data does not include option code and length
-// bytes.
-func ParseOptDHCPv4Msg(data []byte) (*OptDHCPv4Msg, error) {
- var opt OptDHCPv4Msg
+// LongString returns a multi-line string representation of DHCPv4 data.
+func (op *OptDHCPv4Msg) LongString(indent int) string {
+ summary := op.Msg.Summary()
+ ind := strings.Repeat(" ", indent+2)
+ if strings.Contains(summary, "\n") {
+ summary = strings.Replace(summary, "\n ", "\n"+ind, -1)
+ }
+ ind = strings.Repeat(" ", indent)
+ return fmt.Sprintf("%s: {%v%s}", op.Code(), summary, ind)
+}
+
+// FromBytes builds an OptDHCPv4Msg structure from a sequence of bytes. The
+// input data does not include option code and length bytes.
+func (op *OptDHCPv4Msg) FromBytes(data []byte) error {
var err error
- opt.Msg, err = dhcpv4.FromBytes(data)
- return &opt, err
+ op.Msg, err = dhcpv4.FromBytes(data)
+ return err
}
diff --git a/dhcpv6/option_dhcpv4_msg_test.go b/dhcpv6/option_dhcpv4_msg_test.go
index d8d3f6d..e7bb1f6 100644
--- a/dhcpv6/option_dhcpv4_msg_test.go
+++ b/dhcpv6/option_dhcpv4_msg_test.go
@@ -43,7 +43,8 @@ func TestParseOptDHCPv4Msg(t *testing.T) {
// magic cookie, then no options
data = append(data, magicCookie[:]...)
- opt, err := ParseOptDHCPv4Msg(data)
+ var opt OptDHCPv4Msg
+ err := opt.FromBytes(data)
d := opt.Msg
require.NoError(t, err)
require.Equal(t, d.OpCode, dhcpv4.OpcodeBootRequest)
diff --git a/dhcpv6/option_dhcpv4_o_dhcpv6_server.go b/dhcpv6/option_dhcpv4_o_dhcpv6_server.go
index 27a0079..1bd60af 100644
--- a/dhcpv6/option_dhcpv4_o_dhcpv6_server.go
+++ b/dhcpv6/option_dhcpv4_o_dhcpv6_server.go
@@ -30,17 +30,15 @@ func (op *OptDHCP4oDHCP6Server) ToBytes() []byte {
}
func (op *OptDHCP4oDHCP6Server) String() string {
- return fmt.Sprintf("OptDHCP4oDHCP6Server{4o6-servers=%v}", op.DHCP4oDHCP6Servers)
+ return fmt.Sprintf("%s: %v", op.Code(), op.DHCP4oDHCP6Servers)
}
-// ParseOptDHCP4oDHCP6Server builds an OptDHCP4oDHCP6Server structure
-// from a sequence of bytes. The input data does not include option code and length
-// bytes.
-func ParseOptDHCP4oDHCP6Server(data []byte) (*OptDHCP4oDHCP6Server, error) {
- var opt OptDHCP4oDHCP6Server
+// FromBytes builds an OptDHCP4oDHCP6Server structure from a sequence of bytes.
+// The input data does not include option code and length bytes.
+func (op *OptDHCP4oDHCP6Server) FromBytes(data []byte) error {
buf := uio.NewBigEndianBuffer(data)
for buf.Has(net.IPv6len) {
- opt.DHCP4oDHCP6Servers = append(opt.DHCP4oDHCP6Servers, buf.CopyN(net.IPv6len))
+ op.DHCP4oDHCP6Servers = append(op.DHCP4oDHCP6Servers, buf.CopyN(net.IPv6len))
}
- return &opt, buf.FinError()
+ return buf.FinError()
}
diff --git a/dhcpv6/option_dhcpv4_o_dhcpv6_server_test.go b/dhcpv6/option_dhcpv4_o_dhcpv6_server_test.go
index de86594..ecde1dc 100644
--- a/dhcpv6/option_dhcpv4_o_dhcpv6_server_test.go
+++ b/dhcpv6/option_dhcpv4_o_dhcpv6_server_test.go
@@ -1,55 +1,80 @@
package dhcpv6
import (
+ "errors"
+ "fmt"
"net"
+ "reflect"
"testing"
+ "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestParseOptDHCP4oDHCP6Server(t *testing.T) {
- data := []byte{
- 0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, 0xfa, 0xce, 0xb0, 0x0c, 0x00, 0x00, 0x00, 0x35,
- }
- expected := []net.IP{
- net.IP(data),
- }
- opt, err := ParseOptDHCP4oDHCP6Server(data)
- require.NoError(t, err)
- require.Equal(t, expected, opt.DHCP4oDHCP6Servers)
- require.Equal(t, OptionDHCP4oDHCP6Server, opt.Code())
- require.Contains(t, opt.String(), "4o6-servers=[2a03:2880:fffe:c:face:b00c:0:35]", "String() should contain the correct DHCP4-over-DHCP6 server output")
-}
-
-func TestOptDHCP4oDHCP6ServerToBytes(t *testing.T) {
- ip1 := net.ParseIP("2a03:2880:fffe:c:face:b00c:0:35")
- ip2 := net.ParseIP("2001:4860:4860::8888")
- servers := []net.IP{ip1, ip2}
- expected := append([]byte{}, []byte(ip1)...)
- expected = append(expected, []byte(ip2)...)
- opt := OptDHCP4oDHCP6Server{DHCP4oDHCP6Servers: servers}
- require.Equal(t, expected, opt.ToBytes())
-}
+func TestDHCP4oDHCP6ParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want *OptDHCP4oDHCP6Server
+ }{
+ {
+ buf: []byte{
+ 0, 88, // DHCP4oDHCP6 option.
+ 0, 32, // length
+ 0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, 0xfa, 0xce, 0xb0, 0x0c, 0x00, 0x00, 0x00, 0x35,
+ 0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, 0xfa, 0xce, 0xb0, 0x0c, 0x00, 0x00, 0x00, 0x35,
+ },
+ want: &OptDHCP4oDHCP6Server{
+ DHCP4oDHCP6Servers: []net.IP{
+ net.IP{0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, 0xfa, 0xce, 0xb0, 0x0c, 0x00, 0x00, 0x00, 0x35},
+ net.IP{0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, 0xfa, 0xce, 0xb0, 0x0c, 0x00, 0x00, 0x00, 0x35},
+ },
+ },
+ },
+ {
+ buf: []byte{
+ 0, 88, // DHCP4oDHCP6 option.
+ 0, 6, // length
+ 0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe,
+ },
+ err: uio.ErrUnreadBytes,
+ },
+ {
+ buf: []byte{
+ 0, 88, // DHCP4oDHCP6 option.
+ 0, 0, // length
+ },
+ want: &OptDHCP4oDHCP6Server{},
+ },
+ {
+ buf: []byte{0, 88, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.DHCP4oDHCP6Server(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("DHCP4oDHCP6Server = %v, want %v", got, tt.want)
+ }
-func TestParseOptDHCP4oDHCP6ServerParseNoAddr(t *testing.T) {
- data := []byte{
+ if tt.want != nil {
+ var m MessageOptions
+ m.Add(tt.want)
+ got := m.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
}
- var expected []net.IP
- opt, err := ParseOptDHCP4oDHCP6Server(data)
- require.NoError(t, err)
- require.Equal(t, expected, opt.DHCP4oDHCP6Servers)
-}
-
-func TestOptDHCP4oDHCP6ServerToBytesNoAddr(t *testing.T) {
- expected := []byte(nil)
- opt := OptDHCP4oDHCP6Server{}
- require.Equal(t, expected, opt.ToBytes())
}
-func TestParseOptDHCP4oDHCP6ServerParseBogus(t *testing.T) {
- data := []byte{
- 0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, // invalid IPv6 address
- }
- _, err := ParseOptDHCP4oDHCP6Server(data)
- require.Error(t, err, "An invalid IPv6 address should return an error")
+func TestParseOptDHCP4oDHCP6Server(t *testing.T) {
+ opt := OptDHCP4oDHCP6Server{DHCP4oDHCP6Servers: []net.IP{net.ParseIP("2a03:2880:fffe:c:face:b00c:0:35")}}
+ require.Contains(t, opt.String(), "[2a03:2880:fffe:c:face:b00c:0:35]", "String() should contain the correct DHCP4-over-DHCP6 server output")
}
diff --git a/dhcpv6/option_dns.go b/dhcpv6/option_dns.go
index c7bbf83..af9bafe 100644
--- a/dhcpv6/option_dns.go
+++ b/dhcpv6/option_dns.go
@@ -31,17 +31,15 @@ func (op *optDNS) ToBytes() []byte {
}
func (op *optDNS) String() string {
- return fmt.Sprintf("DNS: %v", op.NameServers)
+ return fmt.Sprintf("%s: %v", op.Code(), op.NameServers)
}
-// parseOptDNS builds an optDNS structure
-// from a sequence of bytes. The input data does not include option code and length
-// bytes.
-func parseOptDNS(data []byte) (*optDNS, error) {
- var opt optDNS
+// FromBytes builds an optDNS structure from a sequence of bytes. The input
+// data does not include option code and length bytes.
+func (op *optDNS) FromBytes(data []byte) error {
buf := uio.NewBigEndianBuffer(data)
for buf.Has(net.IPv6len) {
- opt.NameServers = append(opt.NameServers, buf.CopyN(net.IPv6len))
+ op.NameServers = append(op.NameServers, buf.CopyN(net.IPv6len))
}
- return &opt, buf.FinError()
+ return buf.FinError()
}
diff --git a/dhcpv6/option_dns_test.go b/dhcpv6/option_dns_test.go
index 34f22f7..6f836a3 100644
--- a/dhcpv6/option_dns_test.go
+++ b/dhcpv6/option_dns_test.go
@@ -1,40 +1,85 @@
package dhcpv6
import (
+ "errors"
+ "fmt"
"net"
+ "reflect"
"testing"
- "github.com/stretchr/testify/require"
+ "github.com/google/go-cmp/cmp"
+ "github.com/u-root/uio/uio"
)
-func TestParseOptDNS(t *testing.T) {
- data := []byte{
- 0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, 0xfa, 0xce, 0xb0, 0x0c, 0x00, 0x00, 0x00, 0x35,
- }
- expected := []net.IP{
- net.IP(data),
- }
- opt, err := parseOptDNS(data)
- require.NoError(t, err)
- require.Equal(t, expected, opt.NameServers)
- require.Equal(t, OptionDNSRecursiveNameServer, opt.Code())
- require.Contains(t, opt.String(), "DNS: [2a03:2880:fffe:c:face:b00c:0:35]", "String() should contain the correct nameservers output")
-}
-
-func TestOptDNSRecursiveNameServerToBytes(t *testing.T) {
- ns1 := net.ParseIP("2a03:2880:fffe:c:face:b00c:0:35")
- ns2 := net.ParseIP("2001:4860:4860::8888")
- nameservers := []net.IP{ns1, ns2}
- expected := append([]byte{}, []byte(ns1)...)
- expected = append(expected, []byte(ns2)...)
- opt := OptDNS(nameservers...)
- require.Equal(t, expected, opt.ToBytes())
-}
+func TestDNSParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want []net.IP
+ }{
+ {
+ buf: []byte{
+ 0, 23, // DNS
+ 0, 16, // length
+ 0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, 0xfa, 0xce, 0xb0, 0x0c, 0x00, 0x00, 0x00, 0x35,
+ },
+ want: []net.IP{
+ net.IP{0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, 0xfa, 0xce, 0xb0, 0x0c, 0x00, 0x00, 0x00, 0x35},
+ },
+ },
+ {
+ buf: []byte{
+ 0, 23, // DNS
+ 0, 32, // length
+ 0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, 0xfa, 0xce, 0xb0, 0x0c, 0x00, 0x00, 0x00, 0x35,
+ 0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, 0xfa, 0xce, 0xb0, 0x0c, 0x00, 0x00, 0x00, 0x35,
+ },
+ want: []net.IP{
+ net.IP{0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, 0xfa, 0xce, 0xb0, 0x0c, 0x00, 0x00, 0x00, 0x35},
+ net.IP{0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, 0xfa, 0xce, 0xb0, 0x0c, 0x00, 0x00, 0x00, 0x35},
+ },
+ },
+ {
+ buf: nil,
+ want: nil,
+ },
+ {
+ buf: []byte{
+ 0, 23,
+ 0, 8,
+ 0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, // invalid IPv6 address
+ },
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ {
+ buf: []byte{0, 23, 0, 1, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ {
+ buf: []byte{0, 23, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.DNS(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("DNS = %v, want %v", got, tt.want)
+ }
-func TestParseOptDNSBogus(t *testing.T) {
- data := []byte{
- 0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, // invalid IPv6 address
+ if tt.want != nil {
+ var m MessageOptions
+ m.Add(OptDNS(tt.want...))
+ got := m.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
}
- _, err := parseOptDNS(data)
- require.Error(t, err, "An invalid nameserver IPv6 address should return an error")
}
diff --git a/dhcpv6/option_domainsearchlist.go b/dhcpv6/option_domainsearchlist.go
index e71a8d5..fd8aa01 100644
--- a/dhcpv6/option_domainsearchlist.go
+++ b/dhcpv6/option_domainsearchlist.go
@@ -25,17 +25,13 @@ func (op *optDomainSearchList) ToBytes() []byte {
}
func (op *optDomainSearchList) String() string {
- return fmt.Sprintf("DomainSearchList: %s", op.DomainSearchList)
+ return fmt.Sprintf("%s: %s", op.Code(), op.DomainSearchList)
}
-// ParseOptDomainSearchList builds an OptDomainSearchList structure from a sequence
-// of bytes. The input data does not include option code and length bytes.
-func parseOptDomainSearchList(data []byte) (*optDomainSearchList, error) {
- var opt optDomainSearchList
+// FromBytes builds an OptDomainSearchList structure from a sequence of bytes.
+// The input data does not include option code and length bytes.
+func (op *optDomainSearchList) FromBytes(data []byte) error {
var err error
- opt.DomainSearchList, err = rfc1035label.FromBytes(data)
- if err != nil {
- return nil, err
- }
- return &opt, nil
+ op.DomainSearchList, err = rfc1035label.FromBytes(data)
+ return err
}
diff --git a/dhcpv6/option_domainsearchlist_test.go b/dhcpv6/option_domainsearchlist_test.go
index 433f710..3881523 100644
--- a/dhcpv6/option_domainsearchlist_test.go
+++ b/dhcpv6/option_domainsearchlist_test.go
@@ -1,47 +1,84 @@
package dhcpv6
import (
+ "errors"
+ "fmt"
"testing"
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
"github.com/insomniacslk/dhcp/rfc1035label"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestParseOptDomainSearchList(t *testing.T) {
- data := []byte{
- 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0,
- 6, 's', 'u', 'b', 'n', 'e', 't', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'o', 'r', 'g', 0,
- }
- opt, err := parseOptDomainSearchList(data)
- require.NoError(t, err)
- require.Equal(t, OptionDomainSearchList, opt.Code())
- require.Equal(t, 2, len(opt.DomainSearchList.Labels))
- require.Equal(t, "example.com", opt.DomainSearchList.Labels[0])
- require.Equal(t, "subnet.example.org", opt.DomainSearchList.Labels[1])
- require.Contains(t, opt.String(), "example.com subnet.example.org", "String() should contain the correct domain search output")
-}
-
-func TestOptDomainSearchListToBytes(t *testing.T) {
- expected := []byte{
- 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0,
- 6, 's', 'u', 'b', 'n', 'e', 't', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'o', 'r', 'g', 0,
- }
- opt := OptDomainSearchList(
- &rfc1035label.Labels{
- Labels: []string{
- "example.com",
- "subnet.example.org",
+func TestDomainSearchListParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want *rfc1035label.Labels
+ }{
+ {
+ buf: []byte{
+ 0, 24, // Domain Search List option
+ 0, 33, // length
+ 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0,
+ 6, 's', 'u', 'b', 'n', 'e', 't', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'o', 'r', 'g', 0,
+ },
+ want: &rfc1035label.Labels{
+ Labels: []string{
+ "example.com",
+ "subnet.example.org",
+ },
},
},
- )
- require.Equal(t, expected, opt.ToBytes())
-}
+ {
+ buf: []byte{
+ 0, 24, // Domain Search List option
+ 0, 22, // length
+ 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0,
+ 6, 's', 'u', 'b', 'n', 'e', 't', 7, 'e', // truncated
+ },
+ err: rfc1035label.ErrBufferTooShort,
+ },
+ {
+ buf: nil,
+ want: nil,
+ },
+ {
+ buf: []byte{0, 24, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ got := mo.DomainSearchList()
+ if !cmp.Equal(got, tt.want, cmpopts.IgnoreUnexported(rfc1035label.Labels{})) {
+ t.Errorf("DomainSearchList = %v, want %v", got, tt.want)
+ }
-func TestParseOptDomainSearchListInvalidLength(t *testing.T) {
- data := []byte{
- 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0,
- 6, 's', 'u', 'b', 'n', 'e', 't', 7, 'e', // truncated
+ if tt.want != nil {
+ var m MessageOptions
+ m.Add(OptDomainSearchList(tt.want))
+ got := m.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
}
- _, err := parseOptDomainSearchList(data)
- require.Error(t, err, "A truncated OptDomainSearchList should return an error")
+}
+
+func TestOptDomainSearchListString(t *testing.T) {
+ opt := OptDomainSearchList(&rfc1035label.Labels{
+ Labels: []string{
+ "example.com",
+ "subnet.example.org",
+ },
+ })
+ require.Contains(t, opt.String(), "example.com subnet.example.org", "String() should contain the correct domain search output")
}
diff --git a/dhcpv6/option_elapsedtime.go b/dhcpv6/option_elapsedtime.go
index 93e43b9..1441428 100644
--- a/dhcpv6/option_elapsedtime.go
+++ b/dhcpv6/option_elapsedtime.go
@@ -29,14 +29,13 @@ func (op *optElapsedTime) ToBytes() []byte {
}
func (op *optElapsedTime) String() string {
- return fmt.Sprintf("ElapsedTime: %s", op.ElapsedTime)
+ return fmt.Sprintf("%s: %s", op.Code(), op.ElapsedTime)
}
-// build an optElapsedTime structure from a sequence of bytes.
+// FromBytes builds an optElapsedTime structure from a sequence of bytes.
// The input data does not include option code and length bytes.
-func parseOptElapsedTime(data []byte) (*optElapsedTime, error) {
- var opt optElapsedTime
+func (op *optElapsedTime) FromBytes(data []byte) error {
buf := uio.NewBigEndianBuffer(data)
- opt.ElapsedTime = time.Duration(buf.Read16()) * 10 * time.Millisecond
- return &opt, buf.FinError()
+ op.ElapsedTime = time.Duration(buf.Read16()) * 10 * time.Millisecond
+ return buf.FinError()
}
diff --git a/dhcpv6/option_elapsedtime_test.go b/dhcpv6/option_elapsedtime_test.go
index 91fab45..5e95cda 100644
--- a/dhcpv6/option_elapsedtime_test.go
+++ b/dhcpv6/option_elapsedtime_test.go
@@ -1,43 +1,76 @@
package dhcpv6
import (
- "bytes"
+ "errors"
+ "fmt"
+ "reflect"
"testing"
"time"
- "github.com/stretchr/testify/require"
+ "github.com/google/go-cmp/cmp"
+ "github.com/u-root/uio/uio"
)
-func TestOptElapsedTime(t *testing.T) {
- opt, err := parseOptElapsedTime([]byte{0xaa, 0xbb})
- if err != nil {
- t.Fatal(err)
- }
- if elapsedTime := opt.ElapsedTime; elapsedTime != 0xaabb*10*time.Millisecond {
- t.Fatalf("Invalid elapsed time. Expected 0xaabb, got %v", elapsedTime)
- }
-}
+func TestElapsedTimeParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want time.Duration
+ }{
+ {
+ buf: []byte{
+ 0, 8, // Elapsed Time option
+ 0, 2, // length
+ 0, 2,
+ },
+ want: 20 * time.Millisecond,
+ },
+ {
+ buf: []byte{
+ 0, 8, // Elapsed Time option
+ 0, 1, // length
+ 0,
+ },
+ err: uio.ErrBufferTooShort,
+ },
+ {
+ buf: []byte{
+ 0, 8, // Elapsed Time option
+ 0, 3, // length
+ 0, 2, 2,
+ },
+ err: uio.ErrUnreadBytes,
+ },
+ {
+ buf: []byte{0, 8, 0},
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.ElapsedTime(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("ElapsedTime = %v, want %v", got, tt.want)
+ }
-func TestOptElapsedTimeToBytes(t *testing.T) {
- opt := OptElapsedTime(0)
- expected := []byte{0, 0}
- if toBytes := opt.ToBytes(); !bytes.Equal(expected, toBytes) {
- t.Fatalf("Invalid ToBytes output. Expected %v, got %v", expected, toBytes)
+ if tt.err == nil {
+ var m MessageOptions
+ m.Add(OptElapsedTime(tt.want))
+ got := m.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
}
}
func TestOptElapsedTimeString(t *testing.T) {
opt := OptElapsedTime(100 * time.Millisecond)
- expected := "ElapsedTime: 100ms"
+ expected := "Elapsed Time: 100ms"
if optString := opt.String(); optString != expected {
t.Fatalf("Invalid elapsed time string. Expected %v, got %v", expected, optString)
}
}
-
-func TestOptElapsedTimeParseInvalidOption(t *testing.T) {
- _, err := parseOptElapsedTime([]byte{0xaa})
- require.Error(t, err, "A short option should return an error")
-
- _, err = parseOptElapsedTime([]byte{0xaa, 0xbb, 0xcc})
- require.Error(t, err, "An option with too many bytes should return an error")
-}
diff --git a/dhcpv6/option_fqdn.go b/dhcpv6/option_fqdn.go
index 62affc7..adb47fb 100644
--- a/dhcpv6/option_fqdn.go
+++ b/dhcpv6/option_fqdn.go
@@ -29,18 +29,17 @@ func (op *OptFQDN) ToBytes() []byte {
}
func (op *OptFQDN) String() string {
- return fmt.Sprintf("OptFQDN{flags=%d, domainname=%s}", op.Flags, op.DomainName)
+ return fmt.Sprintf("%s: {Flags=%d DomainName=%s}", op.Code(), op.Flags, op.DomainName)
}
-// ParseOptFQDN deserializes from bytes to build a OptFQDN structure.
-func ParseOptFQDN(data []byte) (*OptFQDN, error) {
- var opt OptFQDN
+// FromBytes deserializes from bytes to build a OptFQDN structure.
+func (op *OptFQDN) FromBytes(data []byte) error {
var err error
buf := uio.NewBigEndianBuffer(data)
- opt.Flags = buf.Read8()
- opt.DomainName, err = rfc1035label.FromBytes(buf.ReadAll())
+ op.Flags = buf.Read8()
+ op.DomainName, err = rfc1035label.FromBytes(buf.ReadAll())
if err != nil {
- return nil, err
+ return err
}
- return &opt, buf.FinError()
+ return buf.FinError()
}
diff --git a/dhcpv6/option_fqdn_test.go b/dhcpv6/option_fqdn_test.go
index f698c93..6fca08d 100644
--- a/dhcpv6/option_fqdn_test.go
+++ b/dhcpv6/option_fqdn_test.go
@@ -1,42 +1,88 @@
package dhcpv6
import (
- "bytes"
+ "errors"
+ "fmt"
"testing"
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
"github.com/insomniacslk/dhcp/rfc1035label"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestParseOptFQDN(t *testing.T) {
- data := []byte{
- 0, // Flags
- 4, 'c', 'n', 'o', 's', 9, 'l', 'o', 'c', 'a', 'l',
- 'h', 'o', 's', 't', 0,
- }
- opt, err := ParseOptFQDN(data)
+func TestFQDNParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want *OptFQDN
+ }{
+ {
+ buf: []byte{
+ 0, 39, // FQDN option
+ 0, 34, // length
+ 0, // flags
+ 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0,
+ 6, 's', 'u', 'b', 'n', 'e', 't', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'o', 'r', 'g', 0,
+ },
+ want: &OptFQDN{
+ Flags: 0,
+ DomainName: &rfc1035label.Labels{
+ Labels: []string{
+ "example.com",
+ "subnet.example.org",
+ },
+ },
+ },
+ },
+ {
+ buf: []byte{
+ 0, 39, // FQDN
+ 0, 23, // length
+ 0, // flags
+ 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0,
+ 6, 's', 'u', 'b', 'n', 'e', 't', 7, 'e', // truncated
+ },
+ err: rfc1035label.ErrBufferTooShort,
+ },
+ {
+ buf: nil,
+ want: nil,
+ },
+ {
+ buf: []byte{0, 39, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ got := mo.FQDN()
+ if !cmp.Equal(got, tt.want, cmpopts.IgnoreUnexported(rfc1035label.Labels{})) {
+ t.Errorf("FQDN = %v, want %v", got, tt.want)
+ }
- require.NoError(t, err)
- require.Equal(t, OptionFQDN, opt.Code())
- require.Equal(t, uint8(0), opt.Flags)
- require.Equal(t, "cnos.localhost", opt.DomainName.Labels[0])
- require.Equal(t, "OptFQDN{flags=0, domainname=[cnos.localhost]}", opt.String())
+ if tt.want != nil {
+ var m MessageOptions
+ m.Add(tt.want)
+ got := m.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
+ }
}
-func TestOptFQDNToBytes(t *testing.T) {
- opt := OptFQDN{
- Flags: 0,
+func TestOptFQDNString(t *testing.T) {
+ opt := &OptFQDN{
DomainName: &rfc1035label.Labels{
Labels: []string{"cnos.localhost"},
},
}
- want := []byte{
- 0, // Flags
- 4, 'c', 'n', 'o', 's', 9, 'l', 'o', 'c', 'a', 'l',
- 'h', 'o', 's', 't', 0,
- }
- b := opt.ToBytes()
- if !bytes.Equal(b, want) {
- t.Fatalf("opt.ToBytes()=%v, want %v", b, want)
- }
+ require.Equal(t, "FQDN: {Flags=0 DomainName=[cnos.localhost]}", opt.String())
}
diff --git a/dhcpv6/option_iaaddress.go b/dhcpv6/option_iaaddress.go
index a701f8d..bc56254 100644
--- a/dhcpv6/option_iaaddress.go
+++ b/dhcpv6/option_iaaddress.go
@@ -59,26 +59,30 @@ func (op *OptIAAddress) ToBytes() []byte {
}
func (op *OptIAAddress) String() string {
- return fmt.Sprintf("IAAddress: IP=%v PreferredLifetime=%v ValidLifetime=%v Options=%v",
- op.IPv6Addr, op.PreferredLifetime, op.ValidLifetime, op.Options)
+ return fmt.Sprintf("%s: {IP=%v PreferredLifetime=%v ValidLifetime=%v Options=%v}",
+ op.Code(), op.IPv6Addr, op.PreferredLifetime, op.ValidLifetime, op.Options)
}
-// ParseOptIAAddress builds an OptIAAddress structure from a sequence
-// of bytes. The input data does not include option code and length
-// bytes.
-func ParseOptIAAddress(data []byte) (*OptIAAddress, error) {
- var opt OptIAAddress
+// LongString returns a multi-line string representation of the OptIAAddress data.
+func (op *OptIAAddress) LongString(indent int) string {
+ return fmt.Sprintf("%s: {IP=%v PreferredLifetime=%v ValidLifetime=%v Options=%v}",
+ op.Code(), op.IPv6Addr, op.PreferredLifetime, op.ValidLifetime, op.Options.LongString(indent))
+}
+
+// FromBytes builds an OptIAAddress structure from a sequence of bytes. The
+// input data does not include option code and length bytes.
+func (op *OptIAAddress) FromBytes(data []byte) error {
buf := uio.NewBigEndianBuffer(data)
- opt.IPv6Addr = net.IP(buf.CopyN(net.IPv6len))
+ op.IPv6Addr = net.IP(buf.CopyN(net.IPv6len))
var t1, t2 Duration
t1.Unmarshal(buf)
t2.Unmarshal(buf)
- opt.PreferredLifetime = t1.Duration
- opt.ValidLifetime = t2.Duration
+ op.PreferredLifetime = t1.Duration
+ op.ValidLifetime = t2.Duration
- if err := opt.Options.FromBytes(buf.ReadAll()); err != nil {
- return nil, err
+ if err := op.Options.FromBytes(buf.ReadAll()); err != nil {
+ return err
}
- return &opt, buf.FinError()
+ return buf.FinError()
}
diff --git a/dhcpv6/option_iaaddress_test.go b/dhcpv6/option_iaaddress_test.go
index 26f1732..d77d3ff 100644
--- a/dhcpv6/option_iaaddress_test.go
+++ b/dhcpv6/option_iaaddress_test.go
@@ -1,74 +1,151 @@
package dhcpv6
import (
+ "errors"
+ "fmt"
"net"
+ "reflect"
"testing"
"time"
+ "github.com/google/go-cmp/cmp"
+ "github.com/insomniacslk/dhcp/iana"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestOptIAAddressParse(t *testing.T) {
- ipaddr := []byte{0x24, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
- data := append(ipaddr, []byte{
- 0xa, 0xb, 0xc, 0xd, // preferred lifetime
- 0xe, 0xf, 0x1, 0x2, // valid lifetime
- 0, 8, 0, 2, 0xaa, 0xbb, // options
- }...)
- opt, err := ParseOptIAAddress(data)
- require.NoError(t, err)
- require.Equal(t, net.IP(ipaddr), opt.IPv6Addr)
- require.Equal(t, 0x0a0b0c0d*time.Second, opt.PreferredLifetime)
- require.Equal(t, 0x0e0f0102*time.Second, opt.ValidLifetime)
-}
-
-func TestOptIAAddressParseInvalidTooShort(t *testing.T) {
- data := []byte{
- 0x24, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
- 0xa, 0xb, 0xc, 0xd, // preferred lifetime
- // truncated here
- }
- _, err := ParseOptIAAddress(data)
- require.Error(t, err)
-}
+func TestIAAddressParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want []*OptIAAddress
+ }{
+ {
+ buf: []byte{
+ 0, 5, // IAAddr option
+ 0, 0x18, // length
+ 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, // IPv6
+ 0, 0, 0, 2, // PreferredLifetime
+ 0, 0, 0, 4, // ValidLifetime
+ },
+ want: []*OptIAAddress{
+ &OptIAAddress{
+ IPv6Addr: net.IP{0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0},
+ PreferredLifetime: 2 * time.Second,
+ ValidLifetime: 4 * time.Second,
+ Options: AddressOptions{Options: Options{}},
+ },
+ },
+ },
+ {
+ buf: []byte{
+ 0, 5, // IAAddr option
+ 0, 32, // length
+ 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, // IPv6
+ 0, 0, 0, 2, // PreferredLifetime
+ 0, 0, 0, 4, // ValidLifetime
+ 0, 13, // option status code
+ 0, 4, // length
+ 0, 0, // StatusSuccess,
+ 'O', 'K', // OK
+ },
+ want: []*OptIAAddress{
+ &OptIAAddress{
+ IPv6Addr: net.IP{0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0},
+ PreferredLifetime: 2 * time.Second,
+ ValidLifetime: 4 * time.Second,
+ Options: AddressOptions{Options: Options{
+ &OptStatusCode{StatusCode: iana.StatusSuccess, StatusMessage: "OK"},
+ }},
+ },
+ },
+ },
+ {
+ buf: []byte{
+ 0, 5, // IAAddr option
+ 0, 0x18, // length
+ 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, // IPv6
+ 0, 0, 0, 2, // PreferredLifetime
+ 0, 0, 0, 4, // ValidLifetime
-func TestOptIAAddressParseInvalidBrokenOptions(t *testing.T) {
- data := []byte{
- 0x24, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
- 0xa, 0xb, 0xc, 0xd, // preferred lifetime
- 0xe, 0xf, 0x1, 0x2, // valid lifetime
- 0, 8, 0, 2, 0xaa, // broken options
- }
- _, err := ParseOptIAAddress(data)
- require.Error(t, err)
-}
+ 0, 5, // IAAddr option
+ 0, 0x18, // length
+ 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, // IPv6
+ 0, 0, 0, 2, // PreferredLifetime
+ 0, 0, 0, 4, // ValidLifetime
+ },
+ want: []*OptIAAddress{
+ &OptIAAddress{
+ IPv6Addr: net.IP{0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0},
+ PreferredLifetime: 2 * time.Second,
+ ValidLifetime: 4 * time.Second,
+ Options: AddressOptions{Options: Options{}},
+ },
+ &OptIAAddress{
+ IPv6Addr: net.IP{0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0},
+ PreferredLifetime: 2 * time.Second,
+ ValidLifetime: 4 * time.Second,
+ Options: AddressOptions{Options: Options{}},
+ },
+ },
+ },
+ {
+ buf: []byte{0, 3, 0, 1, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ {
+ buf: []byte{
+ 0, 5, // IAAddr option code
+ 0, 4, // length
+ 0, 0, 0, 1, // truncated IP
+ },
+ want: nil,
+ err: uio.ErrBufferTooShort,
+ },
+ {
+ buf: []byte{
+ 0, 5, // IAAddr option
+ 0, 28, // length
+ 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, // IPv6
+ 0, 0, 0, 2, // PreferredLifetime
+ 0, 0, 0, 4, // ValidLifetime
+ 0, 13, // option status code
+ 0, 1, // length
+ // option too short
+ },
+ err: uio.ErrBufferTooShort,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var io IdentityOptions
+ if err := io.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := io.Addresses(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Addresses = %v, want %v", got, tt.want)
+ }
-func TestOptIAAddressToBytesDefault(t *testing.T) {
- want := []byte{
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // IP
- 0, 0, 0, 0, // preferred lifetime
- 0, 0, 0, 0, // valid lifetime
- }
- opt := OptIAAddress{}
- require.Equal(t, opt.ToBytes(), want)
-}
+ var wantOneAddr *OptIAAddress
+ if len(tt.want) >= 1 {
+ wantOneAddr = tt.want[0]
+ }
+ if got := io.OneAddress(); !reflect.DeepEqual(got, wantOneAddr) {
+ t.Errorf("OneAddress = %v, want %v", got, wantOneAddr)
+ }
-func TestOptIAAddressToBytes(t *testing.T) {
- ipBytes := []byte{0x24, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
- expected := append(ipBytes, []byte{
- 0xa, 0xb, 0xc, 0xd, // preferred lifetime
- 0xe, 0xf, 0x1, 0x2, // valid lifetime
- 0, 8, 0, 2, 0x00, 0x01, // options
- }...)
- opt := OptIAAddress{
- IPv6Addr: net.IP(ipBytes),
- PreferredLifetime: 0x0a0b0c0d * time.Second,
- ValidLifetime: 0x0e0f0102 * time.Second,
- Options: AddressOptions{[]Option{
- OptElapsedTime(10 * time.Millisecond),
- }},
+ if len(tt.want) >= 1 {
+ var b IdentityOptions
+ for _, iana := range tt.want {
+ b.Add(iana)
+ }
+ got := b.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
}
- require.Equal(t, expected, opt.ToBytes())
}
func TestOptIAAddressString(t *testing.T) {
@@ -78,7 +155,8 @@ func TestOptIAAddressString(t *testing.T) {
0x00, 0x00, 0x00, 50, // valid lifetime
0, 8, 0, 2, 0xaa, 0xbb, // options
}...)
- opt, err := ParseOptIAAddress(data)
+ var opt OptIAAddress
+ err := opt.FromBytes(data)
require.NoError(t, err)
str := opt.String()
diff --git a/dhcpv6/option_iapd.go b/dhcpv6/option_iapd.go
index 00bc424..fbf40b4 100644
--- a/dhcpv6/option_iapd.go
+++ b/dhcpv6/option_iapd.go
@@ -18,6 +18,9 @@ type PDOptions struct {
// Prefixes are the prefixes associated with this delegation.
func (po PDOptions) Prefixes() []*OptIAPrefix {
opts := po.Options.Get(OptionIAPrefix)
+ if len(opts) == 0 {
+ return nil
+ }
pre := make([]*OptIAPrefix, 0, len(opts))
for _, o := range opts {
if iap, ok := o.(*OptIAPrefix); ok {
@@ -70,25 +73,29 @@ func (op *OptIAPD) ToBytes() []byte {
// String returns a string representation of the OptIAPD data
func (op *OptIAPD) String() string {
- return fmt.Sprintf("IAPD: {IAID=%v, t1=%v, t2=%v, Options=[%v]}",
- op.IaId, op.T1, op.T2, op.Options)
+ return fmt.Sprintf("%s: {IAID=%#x T1=%v T2=%v Options=%v}",
+ op.Code(), op.IaId, op.T1, op.T2, op.Options)
+}
+
+// LongString returns a multi-line string representation of the OptIAPD data
+func (op *OptIAPD) LongString(indentSpace int) string {
+ return fmt.Sprintf("%s: IAID=%#x T1=%v T2=%v Options=%v", op.Code(), op.IaId, op.T1, op.T2, op.Options.LongString(indentSpace))
}
-// ParseOptIAPD builds an OptIAPD structure from a sequence of bytes.
-// The input data does not include option code and length bytes.
-func ParseOptIAPD(data []byte) (*OptIAPD, error) {
- var opt OptIAPD
+// FromBytes builds an OptIAPD structure from a sequence of bytes. The input
+// data does not include option code and length bytes.
+func (op *OptIAPD) FromBytes(data []byte) error {
buf := uio.NewBigEndianBuffer(data)
- buf.ReadBytes(opt.IaId[:])
+ buf.ReadBytes(op.IaId[:])
var t1, t2 Duration
t1.Unmarshal(buf)
t2.Unmarshal(buf)
- opt.T1 = t1.Duration
- opt.T2 = t2.Duration
+ op.T1 = t1.Duration
+ op.T2 = t2.Duration
- if err := opt.Options.FromBytes(buf.ReadAll()); err != nil {
- return nil, err
+ if err := op.Options.FromBytes(buf.ReadAll()); err != nil {
+ return err
}
- return &opt, buf.FinError()
+ return buf.FinError()
}
diff --git a/dhcpv6/option_iapd_test.go b/dhcpv6/option_iapd_test.go
index b2b9d3f..37a835a 100644
--- a/dhcpv6/option_iapd_test.go
+++ b/dhcpv6/option_iapd_test.go
@@ -1,84 +1,172 @@
package dhcpv6
import (
+ "errors"
+ "fmt"
"net"
+ "reflect"
"testing"
"time"
+ "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestOptIAPDParseOptIAPD(t *testing.T) {
- data := []byte{
- 1, 0, 0, 0, // IAID
- 0, 0, 0, 1, // T1
- 0, 0, 0, 2, // T2
- 0, 26, 0, 25, // 26 = IAPrefix Option, 25 = length
- 0xaa, 0xbb, 0xcc, 0xdd, // IAPrefix preferredLifetime
- 0xee, 0xff, 0x00, 0x11, // IAPrefix validLifetime
- 36, // IAPrefix prefixLength
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // IAPrefix ipv6Prefix
- }
- opt, err := ParseOptIAPD(data)
- require.NoError(t, err)
- require.Equal(t, OptionIAPD, opt.Code())
- require.Equal(t, [4]byte{1, 0, 0, 0}, opt.IaId)
- require.Equal(t, time.Second, opt.T1)
- require.Equal(t, 2*time.Second, opt.T2)
-}
-
-func TestOptIAPDParseOptIAPDInvalidLength(t *testing.T) {
- data := []byte{
- 1, 0, 0, 0, // IAID
- 0, 0, 0, 1, // T1
- // truncated from here
- }
- _, err := ParseOptIAPD(data)
- require.Error(t, err)
-}
-
-func TestOptIAPDParseOptIAPDInvalidOptions(t *testing.T) {
- data := []byte{
- 1, 0, 0, 0, // IAID
- 0, 0, 0, 1, // T1
- 0, 0, 0, 2, // T2
- 0, 26, 0, 25, // 26 = IAPrefix Option, 25 = length
- 0xaa, 0xbb, 0xcc, 0xdd, // IAPrefix preferredLifetime
- 0xee, 0xff, 0x00, 0x11, // IAPrefix validLifetime
- 36, // IAPrefix prefixLength
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // IAPrefix ipv6Prefix missing last byte
- }
- _, err := ParseOptIAPD(data)
- require.Error(t, err)
-}
+func TestIAPDParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want []*OptIAPD
+ }{
+ {
+ buf: []byte{
+ 0, 25, // IAPD option code
+ 0, 41, // length
+ 1, 0, 0, 0, // IAID
+ 0, 0, 0, 1, // T1
+ 0, 0, 0, 2, // T2
+ 0, 26, 0, 25, // 26 = IAPrefix Option, 25 = length
+ 0, 0, 0, 2, // IAPrefix preferredLifetime
+ 0, 0, 0, 4, // IAPrefix validLifetime
+ 36, // IAPrefix prefixLength
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // IAPrefix ipv6Prefix
+ },
+ want: []*OptIAPD{
+ &OptIAPD{
+ IaId: [4]byte{1, 0, 0, 0},
+ T1: 1 * time.Second,
+ T2: 2 * time.Second,
+ Options: PDOptions{Options: Options{&OptIAPrefix{
+ PreferredLifetime: 2 * time.Second,
+ ValidLifetime: 4 * time.Second,
+ Prefix: &net.IPNet{
+ Mask: net.CIDRMask(36, 128),
+ IP: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
+ },
+ Options: PrefixOptions{Options: Options{}},
+ }}},
+ },
+ },
+ },
+ {
+ buf: []byte{
+ 0, 25, // IAPD option code
+ 0, 41, // length
+ 1, 0, 0, 0, // IAID
+ 0, 0, 0, 1, // T1
+ 0, 0, 0, 2, // T2
+ 0, 26, 0, 25, // 26 = IAPrefix Option, 25 = length
+ 0, 0, 0, 2, // IAPrefix preferredLifetime
+ 0, 0, 0, 4, // IAPrefix validLifetime
+ 36, // IAPrefix prefixLength
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // IAPrefix ipv6Prefix
-func TestOptIAPDToBytes(t *testing.T) {
- oaddr := OptIAPrefix{
- PreferredLifetime: 0xaabbccdd * time.Second,
- ValidLifetime: 0xeeff0011 * time.Second,
- Prefix: &net.IPNet{
- Mask: net.CIDRMask(36, 128),
- IP: net.IPv6loopback,
+ 0, 25, // IAPD option code
+ 0, 41, // length
+ 1, 2, 3, 4, // IAID
+ 0, 0, 0, 5, // T1
+ 0, 0, 0, 6, // T2
+ 0, 26, 0, 25, // 26 = IAPrefix Option, 25 = length
+ 0, 0, 0, 2, // IAPrefix preferredLifetime
+ 0, 0, 0, 4, // IAPrefix validLifetime
+ 36, // IAPrefix prefixLength
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // IAPrefix ipv6Prefix
+ },
+ want: []*OptIAPD{
+ &OptIAPD{
+ IaId: [4]byte{1, 0, 0, 0},
+ T1: 1 * time.Second,
+ T2: 2 * time.Second,
+ Options: PDOptions{Options: Options{&OptIAPrefix{
+ PreferredLifetime: 2 * time.Second,
+ ValidLifetime: 4 * time.Second,
+ Prefix: &net.IPNet{
+ Mask: net.CIDRMask(36, 128),
+ IP: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
+ },
+ Options: PrefixOptions{Options: Options{}},
+ }}},
+ },
+ &OptIAPD{
+ IaId: [4]byte{1, 2, 3, 4},
+ T1: 5 * time.Second,
+ T2: 6 * time.Second,
+ Options: PDOptions{Options: Options{&OptIAPrefix{
+ PreferredLifetime: 2 * time.Second,
+ ValidLifetime: 4 * time.Second,
+ Prefix: &net.IPNet{
+ Mask: net.CIDRMask(36, 128),
+ IP: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
+ },
+ Options: PrefixOptions{Options: Options{}},
+ }}},
+ },
+ },
},
- }
- opt := OptIAPD{
- IaId: [4]byte{1, 2, 3, 4},
- T1: 12345 * time.Second,
- T2: 54321 * time.Second,
- Options: PDOptions{[]Option{&oaddr}},
- }
+ {
+ buf: nil,
+ want: nil,
+ },
+ {
+ buf: []byte{0, 25, 0, 1, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ {
+ buf: []byte{
+ 0, 25, // IAPD option code
+ 0, 8, // length
+ 1, 0, 0, 0, // IAID
+ 0, 0, 0, 1, // T1
+ // truncated from here
+ },
+ want: nil,
+ err: uio.ErrBufferTooShort,
+ },
+ {
+ buf: []byte{
+ 0, 25, // IAPD option code
+ 0, 36, // length
+ 1, 0, 0, 0, // IAID
+ 0, 0, 0, 1, // T1
+ 0, 0, 0, 2, // T2
+ 0, 26, 0, 4, // 26 = IAPrefix Option, 4 = length
+ 0, 0, 0, 2, // IAPrefix preferredLifetime
+ // Missing stuff
+ },
+ want: nil,
+ err: uio.ErrBufferTooShort,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.IAPD(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("IAPD = %v, want %v", got, tt.want)
+ }
+ var wantOne *OptIAPD
+ if len(tt.want) >= 1 {
+ wantOne = tt.want[0]
+ }
+ if got := mo.OneIAPD(); !reflect.DeepEqual(got, wantOne) {
+ t.Errorf("OneIAPD = %v, want %v", got, wantOne)
+ }
- expected := []byte{
- 1, 2, 3, 4, // IA ID
- 0, 0, 0x30, 0x39, // T1 = 12345
- 0, 0, 0xd4, 0x31, // T2 = 54321
- 0, 26, 0, 25, // 26 = IAPrefix Option, 25 = length
- 0xaa, 0xbb, 0xcc, 0xdd, // IAPrefix preferredLifetime
- 0xee, 0xff, 0x00, 0x11, // IAPrefix validLifetime
- 36, // IAPrefix prefixLength
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // IAPrefix ipv6Prefix
+ if len(tt.want) >= 1 {
+ var b MessageOptions
+ for _, iana := range tt.want {
+ b.Add(iana)
+ }
+ got := b.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
}
- require.Equal(t, expected, opt.ToBytes())
}
func TestOptIAPDString(t *testing.T) {
@@ -92,23 +180,24 @@ func TestOptIAPDString(t *testing.T) {
36, // IAPrefix prefixLength
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // IAPrefix ipv6Prefix
}
- opt, err := ParseOptIAPD(data)
+ var opt OptIAPD
+ err := opt.FromBytes(data)
require.NoError(t, err)
str := opt.String()
require.Contains(
t, str,
- "IAID=[1 0 0 0]",
+ "IAID=0x01000000",
"String() should return the IAID",
)
require.Contains(
t, str,
- "t1=1s, t2=2s",
+ "T1=1s T2=2s",
"String() should return the T1/T2 options",
)
require.Contains(
t, str,
- "Options=[",
+ "Options={",
"String() should return a list of options",
)
}
diff --git a/dhcpv6/option_iaprefix.go b/dhcpv6/option_iaprefix.go
index 6edacf3..f7d3e76 100644
--- a/dhcpv6/option_iaprefix.go
+++ b/dhcpv6/option_iaprefix.go
@@ -70,35 +70,34 @@ func (op *OptIAPrefix) ToBytes() []byte {
}
func (op *OptIAPrefix) String() string {
- return fmt.Sprintf("IAPrefix: {PreferredLifetime=%v, ValidLifetime=%v, Prefix=%s, Options=%v}",
- op.PreferredLifetime, op.ValidLifetime, op.Prefix, op.Options)
+ return fmt.Sprintf("%s: {PreferredLifetime=%v, ValidLifetime=%v, Prefix=%s, Options=%v}",
+ op.Code(), op.PreferredLifetime, op.ValidLifetime, op.Prefix, op.Options)
}
-// ParseOptIAPrefix an OptIAPrefix structure from a sequence of bytes. The
-// input data does not include option code and length bytes.
-func ParseOptIAPrefix(data []byte) (*OptIAPrefix, error) {
+// FromBytes an OptIAPrefix structure from a sequence of bytes. The input data
+// does not include option code and length bytes.
+func (op *OptIAPrefix) FromBytes(data []byte) error {
buf := uio.NewBigEndianBuffer(data)
- var opt OptIAPrefix
var t1, t2 Duration
t1.Unmarshal(buf)
t2.Unmarshal(buf)
- opt.PreferredLifetime = t1.Duration
- opt.ValidLifetime = t2.Duration
+ op.PreferredLifetime = t1.Duration
+ op.ValidLifetime = t2.Duration
length := buf.Read8()
ip := net.IP(buf.CopyN(net.IPv6len))
if length == 0 {
- opt.Prefix = nil
+ op.Prefix = nil
} else {
- opt.Prefix = &net.IPNet{
+ op.Prefix = &net.IPNet{
Mask: net.CIDRMask(int(length), 128),
IP: ip,
}
}
- if err := opt.Options.FromBytes(buf.ReadAll()); err != nil {
- return nil, err
+ if err := op.Options.FromBytes(buf.ReadAll()); err != nil {
+ return err
}
- return &opt, buf.FinError()
+ return buf.FinError()
}
diff --git a/dhcpv6/option_iaprefix_test.go b/dhcpv6/option_iaprefix_test.go
index 27d0c95..de1960c 100644
--- a/dhcpv6/option_iaprefix_test.go
+++ b/dhcpv6/option_iaprefix_test.go
@@ -1,86 +1,150 @@
package dhcpv6
import (
- "bytes"
+ "errors"
+ "fmt"
"net"
"reflect"
"testing"
"time"
+ "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestOptIAPrefix(t *testing.T) {
- buf := []byte{
- 0xaa, 0xbb, 0xcc, 0xdd, // preferredLifetime
- 0xee, 0xff, 0x00, 0x11, // validLifetime
- 36, // prefixLength
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // ipv6Prefix
- }
- opt, err := ParseOptIAPrefix(buf)
- if err != nil {
- t.Fatal(err)
- }
- want := &OptIAPrefix{
- PreferredLifetime: 0xaabbccdd * time.Second,
- ValidLifetime: 0xeeff0011 * time.Second,
- Prefix: &net.IPNet{
- Mask: net.CIDRMask(36, 128),
- IP: net.IPv6loopback,
+func TestIAPrefixParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want []*OptIAPrefix
+ }{
+ {
+ buf: []byte{
+ 0, 26, // IAPrefix option code
+ 0, 25, // length
+ 0, 0, 0, 1, // PreferredLifetime
+ 0, 0, 0, 2, // ValidLifetime
+ 16, // prefix length
+ 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, // v6-prefix
+ },
+ want: []*OptIAPrefix{
+ &OptIAPrefix{
+ PreferredLifetime: 1 * time.Second,
+ ValidLifetime: 2 * time.Second,
+ Prefix: &net.IPNet{
+ IP: net.IP{0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0},
+ Mask: net.CIDRMask(16, 128),
+ },
+ Options: PrefixOptions{Options: Options{}},
+ },
+ },
},
- Options: PrefixOptions{[]Option{}},
- }
- if !reflect.DeepEqual(want, opt) {
- t.Errorf("parseIAPrefix = %v, want %v", opt, want)
- }
-}
-
-func TestOptIAPrefixToBytes(t *testing.T) {
- buf := []byte{
- 0xaa, 0xbb, 0xcc, 0xdd, // preferredLifetime
- 0xee, 0xff, 0x00, 0x11, // validLifetime
- 36, // prefixLength
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ipv6Prefix
- 0, 8, 0, 2, 0x00, 0x01, // options
- }
- opt := OptIAPrefix{
- PreferredLifetime: 0xaabbccdd * time.Second,
- ValidLifetime: 0xeeff0011 * time.Second,
- Prefix: &net.IPNet{
- Mask: net.CIDRMask(36, 128),
- IP: net.IPv6zero,
+ {
+ buf: []byte{
+ 0, 26, // IAPrefix option code
+ 0, 25, // length
+ 0, 0, 0, 1, // PreferredLifetime
+ 0, 0, 0, 2, // ValidLifetime
+ 0, // prefix length
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // v6-prefix
+ },
+ want: []*OptIAPrefix{
+ &OptIAPrefix{
+ PreferredLifetime: 1 * time.Second,
+ ValidLifetime: 2 * time.Second,
+ Prefix: nil,
+ Options: PrefixOptions{Options: Options{}},
+ },
+ },
},
- Options: PrefixOptions{[]Option{OptElapsedTime(10 * time.Millisecond)}},
- }
- toBytes := opt.ToBytes()
- if !bytes.Equal(toBytes, buf) {
- t.Fatalf("Invalid ToBytes result. Expected %v, got %v", buf, toBytes)
- }
-}
+ {
+ buf: []byte{
+ 0, 26, // IAPrefix option code
+ 0, 25, // length
+ 0, 0, 0, 1, // PreferredLifetime
+ 0, 0, 0, 2, // ValidLifetime
+ 16, // prefix length
+ 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, // v6-prefix
-func TestOptIAPrefixToBytesDefault(t *testing.T) {
- buf := []byte{
- 0, 0, 0, 0, // preferredLifetime
- 0, 0, 0, 0, // validLifetime
- 0, // prefixLength
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ipv6Prefix
- }
- opt := OptIAPrefix{}
- toBytes := opt.ToBytes()
- if !bytes.Equal(toBytes, buf) {
- t.Fatalf("Invalid ToBytes result. Expected %v, got %v", buf, toBytes)
- }
-}
+ 0, 26, // IAPrefix option code
+ 0, 25, // length
+ 0, 0, 0, 15, // PreferredLifetime
+ 0, 0, 0, 14, // ValidLifetime
+ 32, // prefix length
+ 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, // v6-prefix
+ },
+ want: []*OptIAPrefix{
+ &OptIAPrefix{
+ PreferredLifetime: 1 * time.Second,
+ ValidLifetime: 2 * time.Second,
+ Prefix: &net.IPNet{
+ IP: net.IP{0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0},
+ Mask: net.CIDRMask(16, 128),
+ },
+ Options: PrefixOptions{Options: Options{}},
+ },
+ &OptIAPrefix{
+ PreferredLifetime: 15 * time.Second,
+ ValidLifetime: 14 * time.Second,
+ Prefix: &net.IPNet{
+ IP: net.IP{0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0},
+ Mask: net.CIDRMask(32, 128),
+ },
+ Options: PrefixOptions{Options: Options{}},
+ },
+ },
+ },
+ {
+ buf: []byte{0, 3, 0, 1, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ {
+ buf: []byte{
+ 0, 26, // IAPrefix option code
+ 0, 8, // length
+ 1, 0, 0, 0, // T1
+ 0, 0, 0, 1, // T2
+ // truncated from here
+ },
+ want: nil,
+ err: uio.ErrBufferTooShort,
+ },
+ {
+ buf: []byte{
+ 0, 26, // IANA option code
+ 0, 26, // length
+ 0, 0, 0, 1, // T1
+ 0, 0, 0, 2, // T2
+ 8,
+ 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, // IPv6
+ 0, // malformed options
+ },
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo PDOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.Prefixes(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Prefixes = %#v, want %#v", got, tt.want)
+ }
-func TestOptIAPrefixParseInvalidTooShort(t *testing.T) {
- buf := []byte{
- 0xaa, 0xbb, 0xcc, 0xdd, // preferredLifetime
- 0xee, 0xff, 0x00, 0x11, // validLifetime
- 36, // prefixLength
- 0, 0, 0, 0, 0, 0, 0, // truncated ipv6Prefix
- }
- if opt, err := ParseOptIAPrefix(buf); err == nil {
- t.Fatalf("ParseOptIAPrefix: Expected error on truncated option, got %v", opt)
+ if len(tt.want) >= 1 {
+ var b PDOptions
+ for _, iana := range tt.want {
+ b.Add(iana)
+ }
+ got := b.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
}
}
@@ -91,7 +155,8 @@ func TestOptIAPrefixString(t *testing.T) {
36, // prefixLength
0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ipv6Prefix
}
- opt, err := ParseOptIAPrefix(buf)
+ var opt OptIAPrefix
+ err := opt.FromBytes(buf)
require.NoError(t, err)
str := opt.String()
diff --git a/dhcpv6/option_informationrefreshtime.go b/dhcpv6/option_informationrefreshtime.go
index 20d8b5c..e0ba43c 100644
--- a/dhcpv6/option_informationrefreshtime.go
+++ b/dhcpv6/option_informationrefreshtime.go
@@ -32,17 +32,16 @@ func (op *optInformationRefreshTime) ToBytes() []byte {
}
func (op *optInformationRefreshTime) String() string {
- return fmt.Sprintf("InformationRefreshTime: %v", op.InformationRefreshtime)
+ return fmt.Sprintf("%s: %v", op.Code(), op.InformationRefreshtime)
}
-// parseOptInformationRefreshTime builds an optInformationRefreshTime structure from a sequence
-// of bytes. The input data does not include option code and length bytes.
-func parseOptInformationRefreshTime(data []byte) (*optInformationRefreshTime, error) {
- var opt optInformationRefreshTime
+// FromBytes builds an optInformationRefreshTime structure from a sequence of
+// bytes. The input data does not include option code and length bytes.
+func (op *optInformationRefreshTime) FromBytes(data []byte) error {
buf := uio.NewBigEndianBuffer(data)
var irt Duration
irt.Unmarshal(buf)
- opt.InformationRefreshtime = irt.Duration
- return &opt, buf.FinError()
+ op.InformationRefreshtime = irt.Duration
+ return buf.FinError()
}
diff --git a/dhcpv6/option_informationrefreshtime_test.go b/dhcpv6/option_informationrefreshtime_test.go
index 68f0855..1345c28 100644
--- a/dhcpv6/option_informationrefreshtime_test.go
+++ b/dhcpv6/option_informationrefreshtime_test.go
@@ -2,16 +2,75 @@ package dhcpv6
import (
"bytes"
+ "errors"
+ "fmt"
+ "reflect"
"testing"
"time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/u-root/uio/uio"
)
+func TestInformationRefreshTimeParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want time.Duration
+ }{
+ {
+ buf: []byte{
+ 0, 32, // IRT option
+ 0, 4, // length
+ 0, 0, 0, 3,
+ },
+ want: 3 * time.Second,
+ },
+ {
+ buf: []byte{
+ 0, 32, // IRT option
+ 0, 6, // length
+ 0, 0, 0, 3, 0, 0,
+ },
+ err: uio.ErrUnreadBytes,
+ },
+ {
+ buf: []byte{0, 32, 0, 1, 0},
+ err: uio.ErrBufferTooShort,
+ },
+ {
+ buf: []byte{0, 32, 0},
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.InformationRefreshTime(0); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("InformationRefreshTime = %v, want %v", got, tt.want)
+ }
+
+ if tt.err == nil {
+ var m MessageOptions
+ m.Add(OptInformationRefreshTime(tt.want))
+ got := m.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
+ }
+}
+
func TestOptInformationRefreshTime(t *testing.T) {
- opt, err := parseOptInformationRefreshTime([]byte{0xaa, 0xbb, 0xcc, 0xdd})
+ var opt optInformationRefreshTime
+ err := opt.FromBytes([]byte{0xaa, 0xbb, 0xcc, 0xdd})
if err != nil {
t.Fatal(err)
}
- if informationRefreshTime := opt.InformationRefreshtime; informationRefreshTime != time.Duration(0xaabbccdd) * time.Second {
+ if informationRefreshTime := opt.InformationRefreshtime; informationRefreshTime != time.Duration(0xaabbccdd)*time.Second {
t.Fatalf("Invalid information refresh time. Expected 0xaabb, got %v", informationRefreshTime)
}
}
@@ -26,7 +85,7 @@ func TestOptInformationRefreshTimeToBytes(t *testing.T) {
func TestOptInformationRefreshTimeString(t *testing.T) {
opt := OptInformationRefreshTime(3600 * time.Second)
- expected := "InformationRefreshTime: 1h0m0s"
+ expected := "Information Refresh Time: 1h0m0s"
if optString := opt.String(); optString != expected {
t.Fatalf("Invalid elapsed time string. Expected %v, got %v", expected, optString)
}
diff --git a/dhcpv6/option_interfaceid.go b/dhcpv6/option_interfaceid.go
index ce85714..a6debba 100644
--- a/dhcpv6/option_interfaceid.go
+++ b/dhcpv6/option_interfaceid.go
@@ -23,13 +23,12 @@ func (op *optInterfaceID) ToBytes() []byte {
}
func (op *optInterfaceID) String() string {
- return fmt.Sprintf("InterfaceID: %v", op.ID)
+ return fmt.Sprintf("%s: %v", op.Code(), op.ID)
}
-// build an optInterfaceID structure from a sequence of bytes.
-// The input data does not include option code and length bytes.
-func parseOptInterfaceID(data []byte) (*optInterfaceID, error) {
- var opt optInterfaceID
- opt.ID = append([]byte(nil), data...)
- return &opt, nil
+// FromBytes builds an optInterfaceID structure from a sequence of bytes. The
+// input data does not include option code and length bytes.
+func (op *optInterfaceID) FromBytes(data []byte) error {
+ op.ID = append([]byte(nil), data...)
+ return nil
}
diff --git a/dhcpv6/option_interfaceid_test.go b/dhcpv6/option_interfaceid_test.go
index 45c1799..640c06a 100644
--- a/dhcpv6/option_interfaceid_test.go
+++ b/dhcpv6/option_interfaceid_test.go
@@ -1,30 +1,64 @@
package dhcpv6
import (
- "bytes"
+ "errors"
+ "fmt"
+ "reflect"
"testing"
+ "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestParseOptInterfaceID(t *testing.T) {
- expected := []byte("DSLAM01 eth2/1/01/21")
- opt, err := parseOptInterfaceID(expected)
- if err != nil {
- t.Fatal(err)
- }
- if url := opt.ID; !bytes.Equal(url, expected) {
- t.Fatalf("Invalid Interface ID. Expected %v, got %v", expected, url)
+func TestInterfaceIDParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want []byte
+ }{
+ {
+ buf: []byte{
+ 0, 18, // Interface ID
+ 0, 4, // length
+ 'S', 'L', 'A', 'M',
+ },
+ want: []byte("SLAM"),
+ },
+ {
+ buf: []byte{
+ 0, 18,
+ 0, 0,
+ },
+ },
+ {
+ buf: []byte{0, 18, 0},
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var ro RelayOptions
+ if err := ro.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := ro.InterfaceID(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("InterfaceID = %v, want %v", got, tt.want)
+ }
+
+ if tt.want != nil {
+ var m RelayOptions
+ m.Add(OptInterfaceID(tt.want))
+ got := m.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
}
}
func TestOptInterfaceID(t *testing.T) {
- want := []byte("DSLAM01 eth2/1/01/21")
- opt := OptInterfaceID(want)
- if got := opt.ToBytes(); !bytes.Equal(got, want) {
- t.Fatalf("%s.ToBytes() = %v, want %v", opt, got, want)
- }
-
+ opt := OptInterfaceID([]byte("DSLAM01 eth2/1/01/21"))
require.Contains(
t,
opt.String(),
diff --git a/dhcpv6/option_nii.go b/dhcpv6/option_nii.go
index fc9f579..c9953ac 100644
--- a/dhcpv6/option_nii.go
+++ b/dhcpv6/option_nii.go
@@ -59,7 +59,7 @@ func (op *OptNetworkInterfaceID) ToBytes() []byte {
}
func (op *OptNetworkInterfaceID) String() string {
- return fmt.Sprintf("NetworkInterfaceID: %s (Revision %d.%d)", op.Typ, op.Major, op.Minor)
+ return fmt.Sprintf("%s: %s (Revision %d.%d)", op.Code(), op.Typ, op.Major, op.Minor)
}
// FromBytes builds an OptNetworkInterfaceID structure from a sequence of
diff --git a/dhcpv6/option_nontemporaryaddress.go b/dhcpv6/option_nontemporaryaddress.go
index 6e5ddee..2734931 100644
--- a/dhcpv6/option_nontemporaryaddress.go
+++ b/dhcpv6/option_nontemporaryaddress.go
@@ -93,25 +93,29 @@ func (op *OptIANA) ToBytes() []byte {
}
func (op *OptIANA) String() string {
- return fmt.Sprintf("IANA: {IAID=%v, t1=%v, t2=%v, options=%v}",
- op.IaId, op.T1, op.T2, op.Options)
+ return fmt.Sprintf("%s: {IAID=%#x T1=%v T2=%v Options=%v}",
+ op.Code(), op.IaId, op.T1, op.T2, op.Options)
}
-// ParseOptIANA builds an OptIANA structure from a sequence of bytes. The
+// LongString returns a multi-line string representation of IANA data.
+func (op *OptIANA) LongString(indentSpace int) string {
+ return fmt.Sprintf("%s: IAID=%#x T1=%s T2=%s Options=%s", op.Code(), op.IaId, op.T1, op.T2, op.Options.LongString(indentSpace))
+}
+
+// FromBytes builds an OptIANA structure from a sequence of bytes. The
// input data does not include option code and length bytes.
-func ParseOptIANA(data []byte) (*OptIANA, error) {
- var opt OptIANA
+func (op *OptIANA) FromBytes(data []byte) error {
buf := uio.NewBigEndianBuffer(data)
- buf.ReadBytes(opt.IaId[:])
+ buf.ReadBytes(op.IaId[:])
var t1, t2 Duration
t1.Unmarshal(buf)
t2.Unmarshal(buf)
- opt.T1 = t1.Duration
- opt.T2 = t2.Duration
+ op.T1 = t1.Duration
+ op.T2 = t2.Duration
- if err := opt.Options.FromBytes(buf.ReadAll()); err != nil {
- return nil, err
+ if err := op.Options.FromBytes(buf.ReadAll()); err != nil {
+ return err
}
- return &opt, buf.FinError()
+ return buf.FinError()
}
diff --git a/dhcpv6/option_nontemporaryaddress_test.go b/dhcpv6/option_nontemporaryaddress_test.go
index 50cb11b..ed9cebd 100644
--- a/dhcpv6/option_nontemporaryaddress_test.go
+++ b/dhcpv6/option_nontemporaryaddress_test.go
@@ -1,44 +1,158 @@
package dhcpv6
import (
+ "errors"
+ "fmt"
"net"
+ "reflect"
"testing"
"time"
+ "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestOptIANAParseOptIANA(t *testing.T) {
- data := []byte{
- 1, 0, 0, 0, // IAID
- 0, 0, 0, 1, // T1
- 0, 0, 0, 2, // T2
- 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0xb2, 0x7a, 0, 0, 0xc0, 0x8a, // options
- }
- opt, err := ParseOptIANA(data)
- require.NoError(t, err)
- require.Equal(t, OptionIANA, opt.Code())
-}
+func TestIANAParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want []*OptIANA
+ }{
+ {
+ buf: []byte{
+ 0, 3, // IANA option code
+ 0, 40, // length
+ 1, 0, 0, 0, // IAID
+ 0, 0, 0, 1, // T1
+ 0, 0, 0, 2, // T2
+ 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, // IPv6
+ 0, 0, 0, 2, // PreferredLifetime
+ 0, 0, 0, 4, // ValidLifetime
+ },
+ want: []*OptIANA{
+ &OptIANA{
+ IaId: [4]byte{1, 0, 0, 0},
+ T1: 1 * time.Second,
+ T2: 2 * time.Second,
+ Options: IdentityOptions{Options: Options{&OptIAAddress{
+ IPv6Addr: net.IP{0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0},
+ PreferredLifetime: 2 * time.Second,
+ ValidLifetime: 4 * time.Second,
+ Options: AddressOptions{Options: Options{}},
+ }}},
+ },
+ },
+ },
+ {
+ buf: []byte{
+ 0, 3, // IANA option code
+ 0, 40, // length
+ 1, 0, 0, 0, // IAID
+ 0, 0, 0, 1, // T1
+ 0, 0, 0, 2, // T2
+ 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, // IPv6
+ 0, 0, 0, 2, // PreferredLifetime
+ 0, 0, 0, 4, // ValidLifetime
-func TestOptIANAParseOptIANAInvalidLength(t *testing.T) {
- data := []byte{
- 1, 0, 0, 0, // IAID
- 0, 0, 0, 1, // T1
- // truncated from here
- }
- _, err := ParseOptIANA(data)
- require.Error(t, err)
-}
+ 0, 3, // IANA option code
+ 0, 40, // length
+ 1, 2, 3, 4, // IAID
+ 0, 0, 0, 9, // T1
+ 0, 0, 0, 8, // T2
+ 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, // IPv6
+ 0, 0, 0, 2, // PreferredLifetime
+ 0, 0, 0, 4, // ValidLifetime
+ },
+ want: []*OptIANA{
+ &OptIANA{
+ IaId: [4]byte{1, 0, 0, 0},
+ T1: 1 * time.Second,
+ T2: 2 * time.Second,
+ Options: IdentityOptions{Options: Options{&OptIAAddress{
+ IPv6Addr: net.IP{0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0},
+ PreferredLifetime: 2 * time.Second,
+ ValidLifetime: 4 * time.Second,
+ Options: AddressOptions{Options: Options{}},
+ }}},
+ },
+ &OptIANA{
+ IaId: [4]byte{1, 2, 3, 4},
+ T1: 9 * time.Second,
+ T2: 8 * time.Second,
+ Options: IdentityOptions{Options: Options{&OptIAAddress{
+ IPv6Addr: net.IP{0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0},
+ PreferredLifetime: 2 * time.Second,
+ ValidLifetime: 4 * time.Second,
+ Options: AddressOptions{Options: Options{}},
+ }}},
+ },
+ },
+ },
-func TestOptIANAParseOptIANAInvalidOptions(t *testing.T) {
- data := []byte{
- 1, 0, 0, 0, // IAID
- 0, 0, 0, 1, // T1
- 0, 0, 0, 2, // T2
- 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0xb2, 0x7a, // truncated options
+ {
+ buf: nil,
+ want: nil,
+ },
+ {
+ buf: []byte{0, 3, 0, 1, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ {
+ buf: []byte{
+ 0, 3, // IANA option code
+ 0, 8, // length
+ 1, 0, 0, 0, // IAID
+ 0, 0, 0, 1, // T1
+ // truncated from here
+ },
+ want: nil,
+ err: uio.ErrBufferTooShort,
+ },
+ {
+ buf: []byte{
+ 0, 3, // IANA option code
+ 0, 36, // length
+ 1, 0, 0, 0, // IAID
+ 0, 0, 0, 1, // T1
+ 0, 0, 0, 2, // T2
+ 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, // IPv6
+ 0, 0, 0xb2, 0x7a, // PreferredLifetime
+ // Missing ValidLifetime
+ },
+ want: nil,
+ err: uio.ErrBufferTooShort,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.IANA(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("IANA = %v, want %v", got, tt.want)
+ }
+ var wantOne *OptIANA
+ if len(tt.want) >= 1 {
+ wantOne = tt.want[0]
+ }
+ if got := mo.OneIANA(); !reflect.DeepEqual(got, wantOne) {
+ t.Errorf("OneIANA = %v, want %v", got, wantOne)
+ }
+
+ if len(tt.want) >= 1 {
+ var b MessageOptions
+ for _, iana := range tt.want {
+ b.Add(iana)
+ }
+ got := b.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
}
- _, err := ParseOptIANA(data)
- require.Error(t, err)
}
func TestOptIANAGetOneOption(t *testing.T) {
@@ -93,48 +207,33 @@ func TestOptIANADelOption(t *testing.T) {
require.Equal(t, iana2.Options.Options, Options{&optsc})
}
-func TestOptIANAToBytes(t *testing.T) {
- opt := OptIANA{
- IaId: [4]byte{1, 2, 3, 4},
- T1: 12345 * time.Second,
- T2: 54321 * time.Second,
- Options: IdentityOptions{[]Option{
- OptElapsedTime(10 * time.Millisecond),
- }},
- }
- expected := []byte{
- 1, 2, 3, 4, // IA ID
- 0, 0, 0x30, 0x39, // T1 = 12345
- 0, 0, 0xd4, 0x31, // T2 = 54321
- 0, 8, 0, 2, 0x00, 0x01,
- }
- require.Equal(t, expected, opt.ToBytes())
-}
-
func TestOptIANAString(t *testing.T) {
data := []byte{
1, 0, 0, 0, // IAID
0, 0, 0, 1, // T1
0, 0, 0, 2, // T2
- 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0xb2, 0x7a, 0, 0, 0xc0, 0x8a, // options
+ 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, // IPv6
+ 0, 0, 0xb2, 0x7a, // PreferredLifetime
+ 0, 0, 0xc0, 0x8a, // ValidLifetime
}
- opt, err := ParseOptIANA(data)
+ var opt OptIANA
+ err := opt.FromBytes(data)
require.NoError(t, err)
str := opt.String()
require.Contains(
t, str,
- "IAID=[1 0 0 0]",
+ "IAID=0x01000000",
"String() should return the IAID",
)
require.Contains(
t, str,
- "t1=1s, t2=2s",
+ "T1=1s T2=2s",
"String() should return the T1/T2 options",
)
require.Contains(
t, str,
- "options={",
+ "Options={",
"String() should return a list of options",
)
}
diff --git a/dhcpv6/option_ntp_server.go b/dhcpv6/option_ntp_server.go
index a7aafb7..69a3e89 100644
--- a/dhcpv6/option_ntp_server.go
+++ b/dhcpv6/option_ntp_server.go
@@ -18,17 +18,20 @@ func (n *NTPSuboptionSrvAddr) Code() OptionCode {
// ToBytes returns the byte serialization of the suboption.
func (n *NTPSuboptionSrvAddr) ToBytes() []byte {
- buf := uio.NewBigEndianBuffer(nil)
- buf.Write16(uint16(NTPSuboptionSrvAddrCode))
- buf.Write16(uint16(net.IPv6len))
- buf.WriteBytes(net.IP(*n).To16())
- return buf.Data()
+ return net.IP(*n).To16()
}
func (n *NTPSuboptionSrvAddr) String() string {
return fmt.Sprintf("Server Address: %s", net.IP(*n).String())
}
+// FromBytes parses NTP server address from a byte slice p.
+func (n *NTPSuboptionSrvAddr) FromBytes(p []byte) error {
+ buf := uio.NewBigEndianBuffer(p)
+ *n = NTPSuboptionSrvAddr(buf.CopyN(net.IPv6len))
+ return buf.FinError()
+}
+
// NTPSuboptionMCAddr is NTP_SUBOPTION_MC_ADDR according to RFC 5908.
type NTPSuboptionMCAddr net.IP
@@ -39,19 +42,24 @@ func (n *NTPSuboptionMCAddr) Code() OptionCode {
// ToBytes returns the byte serialization of the suboption.
func (n *NTPSuboptionMCAddr) ToBytes() []byte {
- buf := uio.NewBigEndianBuffer(nil)
- buf.Write16(uint16(NTPSuboptionMCAddrCode))
- buf.Write16(uint16(net.IPv6len))
- buf.WriteBytes(net.IP(*n).To16())
- return buf.Data()
+ return net.IP(*n).To16()
}
func (n *NTPSuboptionMCAddr) String() string {
return fmt.Sprintf("Multicast Address: %s", net.IP(*n).String())
}
+// FromBytes parses NTP multicast address from a byte slice p.
+func (n *NTPSuboptionMCAddr) FromBytes(p []byte) error {
+ buf := uio.NewBigEndianBuffer(p)
+ *n = NTPSuboptionMCAddr(buf.CopyN(net.IPv6len))
+ return buf.FinError()
+}
+
// NTPSuboptionSrvFQDN is NTP_SUBOPTION_SRV_FQDN according to RFC 5908.
-type NTPSuboptionSrvFQDN rfc1035label.Labels
+type NTPSuboptionSrvFQDN struct {
+ rfc1035label.Labels
+}
// Code returns the suboption code.
func (n *NTPSuboptionSrvFQDN) Code() OptionCode {
@@ -60,17 +68,16 @@ func (n *NTPSuboptionSrvFQDN) Code() OptionCode {
// ToBytes returns the byte serialization of the suboption.
func (n *NTPSuboptionSrvFQDN) ToBytes() []byte {
- buf := uio.NewBigEndianBuffer(nil)
- buf.Write16(uint16(NTPSuboptionSrvFQDNCode))
- l := rfc1035label.Labels(*n)
- buf.Write16(uint16(l.Length()))
- buf.WriteBytes(l.ToBytes())
- return buf.Data()
+ return n.Labels.ToBytes()
}
func (n *NTPSuboptionSrvFQDN) String() string {
- l := rfc1035label.Labels(*n)
- return fmt.Sprintf("Server FQDN: %s", l.String())
+ return fmt.Sprintf("Server FQDN: %s", n.Labels.String())
+}
+
+// FromBytes parses an NTP server FQDN from a byte slice p.
+func (n *NTPSuboptionSrvFQDN) FromBytes(p []byte) error {
+ return n.Labels.FromBytes(p)
}
// NTPSuboptionSrvAddr is the value of NTP_SUBOPTION_SRV_ADDR according to RFC 5908.
@@ -82,53 +89,18 @@ const (
// parseNTPSuboption implements the OptionParser interface.
func parseNTPSuboption(code OptionCode, data []byte) (Option, error) {
- //var o Options
- buf := uio.NewBigEndianBuffer(data)
- length := len(data)
- data, err := buf.ReadN(length)
- if err != nil {
- return nil, fmt.Errorf("failed to read %d bytes for suboption: %w", length, err)
- }
+ var o Option
switch code {
- case NTPSuboptionSrvAddrCode, NTPSuboptionMCAddrCode:
- if length != net.IPv6len {
- return nil, fmt.Errorf("invalid suboption length, want %d, got %d", net.IPv6len, length)
- }
- var so Option
- switch code {
- case NTPSuboptionSrvAddrCode:
- sos := NTPSuboptionSrvAddr(data)
- so = &sos
- case NTPSuboptionMCAddrCode:
- som := NTPSuboptionMCAddr(data)
- so = &som
- }
- return so, nil
+ case NTPSuboptionSrvAddrCode:
+ o = &NTPSuboptionSrvAddr{}
+ case NTPSuboptionMCAddrCode:
+ o = &NTPSuboptionMCAddr{}
case NTPSuboptionSrvFQDNCode:
- l, err := rfc1035label.FromBytes(data)
- if err != nil {
- return nil, fmt.Errorf("failed to parse rfc1035 labels: %w", err)
- }
- // TODO according to rfc3315, this label must not be compressed.
- // Need to add support for compression detection to the
- // `rfc1035label` package in order to do that.
- so := NTPSuboptionSrvFQDN(*l)
- return &so, nil
+ o = &NTPSuboptionSrvFQDN{}
default:
- gopt := OptionGeneric{OptionCode: code, OptionData: data}
- return &gopt, nil
- }
-}
-
-// ParseOptNTPServer parses a sequence of bytes into an OptNTPServer object.
-func ParseOptNTPServer(data []byte) (*OptNTPServer, error) {
- var so Options
- if err := so.FromBytesWithParser(data, parseNTPSuboption); err != nil {
- return nil, err
+ o = &OptionGeneric{OptionCode: code}
}
- return &OptNTPServer{
- Suboptions: so,
- }, nil
+ return o, o.FromBytes(data)
}
// OptNTPServer is an option NTP server as defined by RFC 5908.
@@ -141,13 +113,14 @@ func (op *OptNTPServer) Code() OptionCode {
return OptionNTPServer
}
+// FromBytes parses a sequence of bytes into an OptNTPServer object.
+func (op *OptNTPServer) FromBytes(data []byte) error {
+ return op.Suboptions.FromBytesWithParser(data, parseNTPSuboption)
+}
+
// ToBytes returns the option serialized to bytes.
func (op *OptNTPServer) ToBytes() []byte {
- buf := uio.NewBigEndianBuffer(nil)
- for _, so := range op.Suboptions {
- buf.WriteBytes(so.ToBytes())
- }
- return buf.Data()
+ return op.Suboptions.ToBytes()
}
func (op *OptNTPServer) String() string {
diff --git a/dhcpv6/option_ntp_server_test.go b/dhcpv6/option_ntp_server_test.go
index 105a753..59b3c69 100644
--- a/dhcpv6/option_ntp_server_test.go
+++ b/dhcpv6/option_ntp_server_test.go
@@ -13,25 +13,22 @@ func TestSuboptionSrvAddr(t *testing.T) {
ip := net.ParseIP("2a03:2880:fffe:c:face:b00c:0:35")
so := NTPSuboptionSrvAddr(ip)
assert.Equal(t, NTPSuboptionSrvAddrCode, so.Code())
- expected := append([]byte{0x00, 0x01, 0x00, 0x10}, ip...)
- assert.Equal(t, expected, so.ToBytes())
+ assert.Equal(t, []byte(ip), so.ToBytes())
}
func TestSuboptionMCAddr(t *testing.T) {
ip := net.ParseIP("2a03:2880:fffe:c:face:b00c:0:35")
so := NTPSuboptionMCAddr(ip)
assert.Equal(t, NTPSuboptionMCAddrCode, so.Code())
- expected := append([]byte{0x00, 0x02, 0x00, 0x10}, ip...)
- assert.Equal(t, expected, so.ToBytes())
+ assert.Equal(t, []byte(ip), so.ToBytes())
}
func TestSuboptionSrvFQDN(t *testing.T) {
fqdn, err := rfc1035label.FromBytes([]byte("\x03ntp\x07example\x03com"))
require.NoError(t, err)
- so := NTPSuboptionSrvFQDN(*fqdn)
+ so := NTPSuboptionSrvFQDN{*fqdn}
assert.Equal(t, NTPSuboptionSrvFQDNCode, so.Code())
- expected := append([]byte{0x00, 0x03, 0x00, 0x10}, fqdn.ToBytes()...)
- assert.Equal(t, expected, so.ToBytes())
+ assert.Equal(t, fqdn.ToBytes(), so.ToBytes())
}
func TestSuboptionGeneric(t *testing.T) {
@@ -40,7 +37,8 @@ func TestSuboptionGeneric(t *testing.T) {
0x00, 0x04, // length, 4 bytes
0x74, 0x65, 0x73, 0x74, // the ASCII bytes for the string "test"
}
- o, err := ParseOptNTPServer(data)
+ var o OptNTPServer
+ err := o.FromBytes(data)
require.NoError(t, err)
require.Equal(t, 1, len(o.Suboptions))
assert.IsType(t, &OptionGeneric{}, o.Suboptions[0])
@@ -67,7 +65,8 @@ func TestParseOptNTPServer(t *testing.T) {
}...)
data = append(data, fqdn.ToBytes()...)
- o, err := ParseOptNTPServer(data)
+ var o OptNTPServer
+ err = o.FromBytes(data)
require.NoError(t, err)
require.NotNil(t, o)
assert.Equal(t, 2, len(o.Suboptions))
@@ -78,11 +77,11 @@ func TestParseOptNTPServer(t *testing.T) {
optFQDN, ok := o.Suboptions[1].(*NTPSuboptionSrvFQDN)
require.True(t, ok)
- assert.Equal(t, *fqdn, rfc1035label.Labels(*optFQDN))
+ assert.Equal(t, *fqdn, optFQDN.Labels)
var mo MessageOptions
assert.Nil(t, mo.NTPServers())
- mo.Add(o)
+ mo.Add(&o)
// MessageOptions.NTPServers only returns server address values.
assert.Equal(t, []net.IP{ip}, mo.NTPServers())
}
diff --git a/dhcpv6/option_relaymsg.go b/dhcpv6/option_relaymsg.go
index 48d9716..9962756 100644
--- a/dhcpv6/option_relaymsg.go
+++ b/dhcpv6/option_relaymsg.go
@@ -25,17 +25,18 @@ func (op *optRelayMsg) ToBytes() []byte {
}
func (op *optRelayMsg) String() string {
- return fmt.Sprintf("RelayMsg: %v", op.Msg)
+ return fmt.Sprintf("%s: %v", op.Code(), op.Msg)
}
-// build an optRelayMsg structure from a sequence of bytes.
-// The input data does not include option code and length bytes.
-func parseOptRelayMsg(data []byte) (*optRelayMsg, error) {
+// LongString returns a multi-line string representation of the relay message data.
+func (op *optRelayMsg) LongString(indent int) string {
+ return fmt.Sprintf("%s: %v", op.Code(), op.Msg.LongString(indent))
+}
+
+// FromBytes build an optRelayMsg structure from a sequence of bytes. The input
+// data does not include option code and length bytes.
+func (op *optRelayMsg) FromBytes(data []byte) error {
var err error
- var opt optRelayMsg
- opt.Msg, err = FromBytes(data)
- if err != nil {
- return nil, err
- }
- return &opt, nil
+ op.Msg, err = FromBytes(data)
+ return err
}
diff --git a/dhcpv6/option_relaymsg_test.go b/dhcpv6/option_relaymsg_test.go
index b898a75..b96c0f9 100644
--- a/dhcpv6/option_relaymsg_test.go
+++ b/dhcpv6/option_relaymsg_test.go
@@ -1,110 +1,78 @@
package dhcpv6
import (
+ "errors"
+ "fmt"
"reflect"
"testing"
- "time"
+ "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestRelayMsgParseOptRelayMsg(t *testing.T) {
- opt, err := parseOptRelayMsg([]byte{
- 1, // MessageTypeSolicit
- 0xaa, 0xbb, 0xcc, // transaction ID
- 0, 8, // option: elapsed time
- 0, 2, // option length
- 0, 0, // option value
- })
- if err != nil {
- t.Fatal(err)
- }
- if code := opt.Code(); code != OptionRelayMsg {
- t.Fatalf("Invalid option code. Expected OptionRelayMsg (%v), got %v",
- OptionRelayMsg, code,
- )
- }
-}
-
-func TestRelayMsgOptionsFromBytes(t *testing.T) {
- var opts Options
- err := opts.FromBytes([]byte{
- 0, 9, // option: relay message
- 0, 10, // relayed message length
- 1, // MessageTypeSolicit
- 0xaa, 0xbb, 0xcc, // transaction ID
- 0, 8, // option: elapsed time
- 0, 2, // option length
- 0, 0, // option value
- })
- if err != nil {
- t.Fatal(err)
- }
- if len(opts) != 1 {
- t.Fatalf("Invalid number of options. Expected 1, got %v", len(opts))
- }
- opt := opts[0]
- if code := opt.Code(); code != OptionRelayMsg {
- t.Fatalf("Invalid option code. Expected OptionRelayMsg (%v), got %v",
- OptionRelayMsg, code,
- )
- }
-}
+func TestRelayMsgOptionParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want DHCPv6
+ }{
+ {
+ buf: []byte{
+ 0, 9, // Relay Msg option
+ 0, 10, // length
+ 1, // MessageTypeSolicit
+ 0xaa, 0xbb, 0xcc, // transaction ID
+ 0, 8, // option: elapsed time
+ 0, 2, // option length
+ 0, 0, // option value
+ },
+ want: &Message{
+ MessageType: MessageTypeSolicit,
+ TransactionID: TransactionID{0xaa, 0xbb, 0xcc},
+ Options: MessageOptions{Options{OptElapsedTime(0)}},
+ },
+ },
+ {
+ buf: []byte{
+ 0, 9, // Relay Msg option
+ 0, 6, // length
+ 1, // MessageTypeSolicit
+ 0xaa, 0xbb, 0xcc, // transaction ID
+ 0, 8, // option: elapsed time
+ // no length/value for elapsed time option
+ },
+ err: uio.ErrUnreadBytes,
+ },
+ {
+ buf: []byte{0, 9, 0, 1, 0},
+ want: nil,
+ err: uio.ErrBufferTooShort,
+ },
+ {
+ buf: []byte{0, 9, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var ro RelayOptions
+ if err := ro.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := ro.RelayMessage(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("RelayMessage = %v, want %v", got, tt.want)
+ }
-func TestRelayMsgParseOptRelayMsgSingleEncapsulation(t *testing.T) {
- d, err := FromBytes([]byte{
- 12, // RELAY-FORW
- 0, // hop count
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // linkAddr
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, // peerAddr
- 0, 9, // option: relay message
- 0, 10, // relayed message length
- 1, // MessageTypeSolicit
- 0xaa, 0xbb, 0xcc, // transaction ID
- 0, 8, // option: elapsed time
- 0, 2, // option length
- 0x00, 0x01, // option value
- })
- if err != nil {
- t.Fatal(err)
- }
- r, ok := d.(*RelayMessage)
- if !ok {
- t.Fatalf("Invalid DHCPv6 type. Expected RelayMessage, got %v",
- reflect.TypeOf(d),
- )
- }
- if mType := r.Type(); mType != MessageTypeRelayForward {
- t.Fatalf("Invalid messge type for relay. Expected %v, got %v", MessageTypeRelayForward, mType)
- }
- if len(r.Options.Options) != 1 {
- t.Fatalf("Invalid number of options. Expected 1, got %v", len(r.Options.Options))
- }
- ro := r.Options.RelayMessage()
- if ro == nil {
- t.Fatalf("No relay message")
- }
- innerDHCP, ok := ro.(*Message)
- if !ok {
- t.Fatalf("Invalid relay message type. Expected Message, got %v",
- reflect.TypeOf(innerDHCP),
- )
- }
- if dType := innerDHCP.Type(); dType != MessageTypeSolicit {
- t.Fatalf("Invalid inner DHCP type. Expected MessageTypeSolicit (%v), got %v",
- MessageTypeSolicit, dType,
- )
- }
- xid := TransactionID{0xaa, 0xbb, 0xcc}
- if tID := innerDHCP.TransactionID; tID != xid {
- t.Fatalf("Invalid inner DHCP transaction ID. Expected 0xaabbcc, got %v", tID)
- }
- if len(innerDHCP.Options.Options) != 1 {
- t.Fatalf("Invalid inner DHCP options length. Expected 1, got %v", len(innerDHCP.Options.Options))
- }
- eTime := innerDHCP.Options.ElapsedTime()
- if eTime != 10*time.Millisecond {
- t.Fatalf("Invalid elapsed time. Expected 0x1122, got 0x%04x", eTime)
+ if tt.want != nil {
+ var m RelayOptions
+ m.Add(OptRelayMessage(tt.want))
+ got := m.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
}
}
@@ -148,17 +116,14 @@ func TestSample(t *testing.T) {
}
func TestRelayMsgParseOptRelayMsgTooShort(t *testing.T) {
- _, err := parseOptRelayMsg([]byte{
- 1, // MessageTypeSolicit
- 0xaa, 0xbb, 0xcc, // transaction ID
- 0, 8, // option: elapsed time
- // no length/value for elapsed time option
- })
+ var opt optRelayMsg
+ err := opt.FromBytes([]byte{})
require.Error(t, err, "ParseOptRelayMsg() should return an error if the encapsulated message is invalid")
}
func TestRelayMsgString(t *testing.T) {
- opt, err := parseOptRelayMsg([]byte{
+ var opt optRelayMsg
+ err := opt.FromBytes([]byte{
1, // MessageTypeSolicit
0xaa, 0xbb, 0xcc, // transaction ID
0, 8, // option: elapsed time
@@ -169,7 +134,7 @@ func TestRelayMsgString(t *testing.T) {
require.Contains(
t,
opt.String(),
- "RelayMsg: Message",
+ "Relay Message: Message",
"String() should contain the relaymsg contents",
)
}
diff --git a/dhcpv6/option_relayport.go b/dhcpv6/option_relayport.go
index fd51948..8bf9eff 100644
--- a/dhcpv6/option_relayport.go
+++ b/dhcpv6/option_relayport.go
@@ -29,14 +29,13 @@ func (op *optRelayPort) ToBytes() []byte {
}
func (op *optRelayPort) String() string {
- return fmt.Sprintf("RelayPort: %d", op.DownstreamSourcePort)
+ return fmt.Sprintf("%s: %d", op.Code(), op.DownstreamSourcePort)
}
-// build an optRelayPort structure from a sequence of bytes.
-// The input data does not include option code and length bytes.
-func parseOptRelayPort(data []byte) (*optRelayPort, error) {
- var opt optRelayPort
+// FromBytes build an optRelayPort structure from a sequence of bytes. The
+// input data does not include option code and length bytes.
+func (op *optRelayPort) FromBytes(data []byte) error {
buf := uio.NewBigEndianBuffer(data)
- opt.DownstreamSourcePort = buf.Read16()
- return &opt, buf.FinError()
+ op.DownstreamSourcePort = buf.Read16()
+ return buf.FinError()
}
diff --git a/dhcpv6/option_relayport_test.go b/dhcpv6/option_relayport_test.go
index 124af62..d80e268 100644
--- a/dhcpv6/option_relayport_test.go
+++ b/dhcpv6/option_relayport_test.go
@@ -7,9 +7,10 @@ import (
)
func TestParseRelayPort(t *testing.T) {
- opt, err := parseOptRelayPort([]byte{0x12, 0x32})
+ var opt optRelayPort
+ err := opt.FromBytes([]byte{0x12, 0x32})
require.NoError(t, err)
- require.Equal(t, &optRelayPort{DownstreamSourcePort: 0x1232}, opt)
+ require.Equal(t, optRelayPort{DownstreamSourcePort: 0x1232}, opt)
}
func TestRelayPortToBytes(t *testing.T) {
diff --git a/dhcpv6/option_remoteid.go b/dhcpv6/option_remoteid.go
index fb028e6..7a3b3b4 100644
--- a/dhcpv6/option_remoteid.go
+++ b/dhcpv6/option_remoteid.go
@@ -26,17 +26,16 @@ func (op *OptRemoteID) ToBytes() []byte {
}
func (op *OptRemoteID) String() string {
- return fmt.Sprintf("RemoteID: EnterpriseNumber %d RemoteID %v",
- op.EnterpriseNumber, op.RemoteID,
+ return fmt.Sprintf("%s: {EnterpriseNumber=%d RemoteID=%#x}",
+ op.Code(), op.EnterpriseNumber, op.RemoteID,
)
}
-// ParseOptRemoteId builds an OptRemoteId structure from a sequence of bytes.
-// The input data does not include option code and length bytes.
-func ParseOptRemoteID(data []byte) (*OptRemoteID, error) {
- var opt OptRemoteID
+// FromBytes builds an OptRemoteID structure from a sequence of bytes. The
+// input data does not include option code and length bytes.
+func (op *OptRemoteID) FromBytes(data []byte) error {
buf := uio.NewBigEndianBuffer(data)
- opt.EnterpriseNumber = buf.Read32()
- opt.RemoteID = buf.ReadAll()
- return &opt, buf.FinError()
+ op.EnterpriseNumber = buf.Read32()
+ op.RemoteID = buf.ReadAll()
+ return buf.FinError()
}
diff --git a/dhcpv6/option_remoteid_test.go b/dhcpv6/option_remoteid_test.go
index 5c1ee52..4cdd37d 100644
--- a/dhcpv6/option_remoteid_test.go
+++ b/dhcpv6/option_remoteid_test.go
@@ -1,64 +1,94 @@
package dhcpv6
import (
- "bytes"
+ "errors"
+ "fmt"
+ "reflect"
"testing"
+ "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestOptRemoteID(t *testing.T) {
- expected := []byte{0xaa, 0xbb, 0xcc, 0xdd}
- remoteId := []byte("DSLAM01 eth2/1/01/21")
- expected = append(expected, remoteId...)
- opt, err := ParseOptRemoteID(expected)
- if err != nil {
- t.Fatal(err)
- }
- if en := opt.EnterpriseNumber; en != 0xaabbccdd {
- t.Fatalf("Invalid Enterprise Number. Expected 0xaabbccdd, got %v", en)
- }
- if rid := opt.RemoteID; !bytes.Equal(rid, remoteId) {
- t.Fatalf("Invalid Remote ID. Expected %v, got %v", expected, rid)
- }
-}
+func TestRemoteIDParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want *OptRemoteID
+ }{
+ {
+ buf: []byte{
+ 0, 37, // Remote ID
+ 0, 8, // length
+ 0, 0, 0, 16,
+ 'S', 'L', 'A', 'M',
+ },
+ want: &OptRemoteID{
+ EnterpriseNumber: 16,
+ RemoteID: []byte("SLAM"),
+ },
+ },
+ {
+ buf: []byte{
+ 0, 37,
+ 0, 0,
+ },
+ err: uio.ErrBufferTooShort,
+ },
+ {
+ buf: []byte{
+ 0, 37,
+ 0, 4,
+ 0, 0, 0, 6,
+ },
+ want: &OptRemoteID{
+ EnterpriseNumber: 6,
+ RemoteID: []byte{},
+ },
+ },
+ {
+ buf: []byte{0, 37, 0},
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var ro RelayOptions
+ if err := ro.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := ro.RemoteID(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("RemoteID = %v, want %v", got, tt.want)
+ }
-func TestOptRemoteIDToBytes(t *testing.T) {
- remoteId := []byte("DSLAM01 eth2/1/01/21")
- expected := append([]byte{0, 0, 0, 0}, remoteId...)
- opt := OptRemoteID{
- RemoteID: remoteId,
- }
- toBytes := opt.ToBytes()
- if !bytes.Equal(toBytes, expected) {
- t.Fatalf("Invalid ToBytes result. Expected %v, got %v", expected, toBytes)
+ if tt.want != nil {
+ var m RelayOptions
+ m.Add(tt.want)
+ got := m.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
}
}
-func TestOptRemoteIDParseOptRemoteIDTooShort(t *testing.T) {
- buf := []byte{0xaa, 0xbb, 0xcc}
- _, err := ParseOptRemoteID(buf)
- require.Error(t, err, "A short option should return an error")
-}
-
func TestOptRemoteIDString(t *testing.T) {
- buf := []byte{0xaa, 0xbb, 0xcc, 0xdd}
- remoteId := []byte("Test1234")
- buf = append(buf, remoteId...)
-
- opt, err := ParseOptRemoteID(buf)
- require.NoError(t, err)
+ opt := &OptRemoteID{
+ EnterpriseNumber: 123,
+ RemoteID: []byte("Test1234"),
+ }
str := opt.String()
require.Contains(
t,
str,
- "EnterpriseNumber 2864434397",
+ "EnterpriseNumber=123",
"String() should contain the enterprisenum",
)
require.Contains(
t,
str,
- "RemoteID [84 101 115 116 49 50 51 52]",
+ "RemoteID=0x5465737431323334",
"String() should contain the remoteid bytes",
)
}
diff --git a/dhcpv6/option_requestedoption.go b/dhcpv6/option_requestedoption.go
index 3281a61..bba5e48 100644
--- a/dhcpv6/option_requestedoption.go
+++ b/dhcpv6/option_requestedoption.go
@@ -71,5 +71,5 @@ func (*optRequestedOption) Code() OptionCode {
}
func (op *optRequestedOption) String() string {
- return fmt.Sprintf("RequestedOptions: %s", op.OptionCodes)
+ return fmt.Sprintf("%s: %s", op.Code(), op.OptionCodes)
}
diff --git a/dhcpv6/option_requestedoption_test.go b/dhcpv6/option_requestedoption_test.go
index 9941a89..ab6e86b 100644
--- a/dhcpv6/option_requestedoption_test.go
+++ b/dhcpv6/option_requestedoption_test.go
@@ -1,23 +1,92 @@
package dhcpv6
import (
+ "errors"
+ "fmt"
+ "reflect"
"testing"
+ "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestOptRequestedOption(t *testing.T) {
- expected := []byte{0, 1, 0, 2}
- var o optRequestedOption
- err := o.FromBytes(expected)
- require.NoError(t, err, "ParseOptRequestedOption() correct options should not error")
+func TestOROParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want OptionCodes
+ }{
+ {
+ buf: []byte{
+ 0, 6, // ORO option
+ 0, 2, // length
+ 0, 3, // IANA option
+ },
+ want: OptionCodes{OptionIANA},
+ },
+ {
+ buf: []byte{
+ 0, 6, // ORO option
+ 0, 4, // length
+ 0, 3, // IANA
+ 0, 4, // IATA
+ },
+ want: OptionCodes{OptionIANA, OptionIATA},
+ },
+ {
+ buf: nil,
+ want: nil,
+ },
+ {
+ buf: []byte{0, 6, 0, 1, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ {
+ buf: []byte{0, 6, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.RequestedOptions(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("RequestedOptions = %v, want %v", got, tt.want)
+ }
+
+ if tt.want != nil {
+ var m MessageOptions
+ m.Add(OptRequestedOption(tt.want...))
+ got := m.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
+ }
}
-func TestOptRequestedOptionParseOptRequestedOptionTooShort(t *testing.T) {
- buf := []byte{0, 1, 0}
- var o optRequestedOption
- err := o.FromBytes(buf)
- require.Error(t, err, "A short option should return an error (must be divisible by 2)")
+func TestParseMessageOptionsWithORO(t *testing.T) {
+ buf := []byte{
+ 0, 6, // ORO option
+ 0, 2, // length
+ 0, 3, // IANA Option
+ 0, 6, // ORO
+ 0, 2, // length
+ 0, 4, // IATA
+ }
+
+ want := OptionCodes{OptionIANA, OptionIATA}
+ var mo MessageOptions
+ if err := mo.FromBytes(buf); err != nil {
+ t.Errorf("FromBytes = %v", err)
+ } else if got := mo.RequestedOptions(); !reflect.DeepEqual(got, want) {
+ t.Errorf("ORO = %v, want %v", got, want)
+ }
}
func TestOptRequestedOptionString(t *testing.T) {
@@ -28,7 +97,7 @@ func TestOptRequestedOptionString(t *testing.T) {
require.Contains(
t,
o.String(),
- "Client Identifier, Server Identifier",
+ "Client ID, Server ID",
"String() should contain the options specified",
)
o.OptionCodes = append(o.OptionCodes, 12345)
diff --git a/dhcpv6/option_serverid.go b/dhcpv6/option_serverid.go
index c2c10da..4c35cc1 100644
--- a/dhcpv6/option_serverid.go
+++ b/dhcpv6/option_serverid.go
@@ -6,12 +6,12 @@ import (
// OptServerID represents a Server Identifier option as defined by RFC 3315
// Section 22.1.
-func OptServerID(d Duid) Option {
+func OptServerID(d DUID) Option {
return &optServerID{d}
}
type optServerID struct {
- Duid
+ DUID
}
func (*optServerID) Code() OptionCode {
@@ -19,15 +19,13 @@ func (*optServerID) Code() OptionCode {
}
func (op *optServerID) String() string {
- return fmt.Sprintf("ServerID: %v", op.Duid.String())
+ return fmt.Sprintf("%s: %v", op.Code(), op.DUID)
}
-// parseOptServerID builds an optServerID structure from a sequence of bytes.
-// The input data does not include option code and length bytes.
-func parseOptServerID(data []byte) (*optServerID, error) {
- sid, err := DuidFromBytes(data)
- if err != nil {
- return nil, err
- }
- return &optServerID{*sid}, nil
+// FromBytes builds an optServerID structure from a sequence of bytes. The
+// input data does not include option code and length bytes.
+func (op *optServerID) FromBytes(data []byte) error {
+ var err error
+ op.DUID, err = DUIDFromBytes(data)
+ return err
}
diff --git a/dhcpv6/option_serverid_test.go b/dhcpv6/option_serverid_test.go
index 05158c4..bb4a928 100644
--- a/dhcpv6/option_serverid_test.go
+++ b/dhcpv6/option_serverid_test.go
@@ -1,84 +1,119 @@
package dhcpv6
import (
+ "errors"
+ "fmt"
"net"
+ "reflect"
"testing"
+ "github.com/google/go-cmp/cmp"
"github.com/insomniacslk/dhcp/iana"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestParseOptServerID(t *testing.T) {
- data := []byte{
- 0, 3, // DUID_LL
- 0, 1, // hwtype ethernet
- 0, 1, 2, 3, 4, 5, // hw addr
+func TestServerIDParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want DUID
+ }{
+ {
+ buf: []byte{
+ 0, 2, // Server ID option
+ 0, 10, // length
+ 0, 3, // DUID_LL
+ 0, 1, // hwtype ethernet
+ 0, 1, 2, 3, 4, 5, // HW addr
+ },
+ want: &DUIDLL{HWType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr{0, 1, 2, 3, 4, 5}},
+ },
+ {
+ buf: nil,
+ want: nil,
+ },
+ {
+ buf: []byte{0, 1, 0, 1, 0},
+ want: nil,
+ err: uio.ErrBufferTooShort,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.ServerID(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("ServerID = %v, want %v", got, tt.want)
+ }
+ })
}
- opt, err := parseOptServerID(data)
- require.NoError(t, err)
- require.Equal(t, DUID_LL, opt.Type)
- require.Equal(t, iana.HWTypeEthernet, opt.HwType)
- require.Equal(t, net.HardwareAddr{0, 1, 2, 3, 4, 5}, opt.LinkLayerAddr)
}
-func TestOptServerIdToBytes(t *testing.T) {
- opt := OptServerID(
- Duid{
- Type: DUID_LL,
- HwType: iana.HWTypeEthernet,
- LinkLayerAddr: net.HardwareAddr{5, 4, 3, 2, 1, 0},
+func TestServerID(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ want optServerID
+ err error
+ }{
+ {
+ buf: []byte{
+ 0, 3, // DUID_LL
+ 0, 1, // hwtype ethernet
+ 0, 1, 2, 3, 4, 5, // hw addr
+ },
+ want: optServerID{
+ &DUIDLL{
+ HWType: iana.HWTypeEthernet,
+ LinkLayerAddr: net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5}),
+ },
+ },
},
- )
- expected := []byte{
- 0, 3, // DUID_LL
- 0, 1, // hwtype ethernet
- 5, 4, 3, 2, 1, 0, // hw addr
- }
- require.Equal(t, expected, opt.ToBytes())
-}
+ {
+ buf: []byte{0},
+ err: uio.ErrBufferTooShort,
+ },
+ {
+ buf: []byte{0, 3, 0},
+ err: uio.ErrBufferTooShort,
+ },
+ {
+ buf: nil,
+ err: uio.ErrBufferTooShort,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var opt optServerID
+ if err := opt.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if tt.err == nil {
+ if !reflect.DeepEqual(tt.want, opt) {
+ t.Errorf("FromBytes = %v, want %v", opt, tt.want)
+ }
-func TestOptServerIdDecodeEncode(t *testing.T) {
- data := []byte{
- 0, 3, // DUID_LL
- 0, 1, // hwtype ethernet
- 5, 4, 3, 2, 1, 0, // hw addr
+ out := tt.want.ToBytes()
+ if diff := cmp.Diff(tt.buf, out); diff != "" {
+ t.Errorf("ToBytes mismatch: (-want, +got):\n%s", diff)
+ }
+ }
+ })
}
- opt, err := parseOptServerID(data)
- require.NoError(t, err)
- require.Equal(t, data, opt.ToBytes())
}
-func TestOptionServerId(t *testing.T) {
+func TestOptionServerIDString(t *testing.T) {
opt := OptServerID(
- Duid{
- Type: DUID_LL,
- HwType: iana.HWTypeEthernet,
- LinkLayerAddr: net.HardwareAddr{0xde, 0xad, 0, 0, 0xbe, 0xef},
+ &DUIDLL{
+ HWType: iana.HWTypeEthernet,
+ LinkLayerAddr: net.HardwareAddr([]byte{0xde, 0xad, 0, 0, 0xbe, 0xef}),
},
)
require.Equal(t, OptionServerID, opt.Code())
require.Contains(
t,
opt.String(),
- "ServerID: DUID{type=DUID-LL hwtype=Ethernet hwaddr=de:ad:00:00:be:ef}",
- "String() should contain the correct sid output",
+ "Server ID: DUID-LL{HWType=Ethernet HWAddr=de:ad:00:00:be:ef}",
+ "String() should contain the correct cid output",
)
}
-
-func TestOptServerIdparseOptServerIDBogusDUID(t *testing.T) {
- data := []byte{
- 0, 4, // DUID_UUID
- 1, 2, 3, 4, 5, 6, 7, 8, 9, // a UUID should be 18 bytes not 17
- 10, 11, 12, 13, 14, 15, 16, 17,
- }
- _, err := parseOptServerID(data)
- require.Error(t, err, "A truncated OptServerId DUID should return an error")
-}
-
-func TestOptServerIdparseOptServerIDInvalidTooShort(t *testing.T) {
- data := []byte{
- 0, // truncated: DUIDs are at least 2 bytes
- }
- _, err := parseOptServerID(data)
- require.Error(t, err, "A truncated OptServerId should return an error")
-}
diff --git a/dhcpv6/option_statuscode.go b/dhcpv6/option_statuscode.go
index b6b5a14..c745bc1 100644
--- a/dhcpv6/option_statuscode.go
+++ b/dhcpv6/option_statuscode.go
@@ -31,16 +31,15 @@ func (op *OptStatusCode) ToBytes() []byte {
// String returns a human-readable option.
func (op *OptStatusCode) String() string {
- return fmt.Sprintf("StatusCode: Code: %s (%d); Message: %s",
- op.StatusCode, op.StatusCode, op.StatusMessage)
+ return fmt.Sprintf("%s: {Code=%s (%d); Message=%s}",
+ op.Code(), op.StatusCode, op.StatusCode, op.StatusMessage)
}
-// ParseOptStatusCode builds an OptStatusCode structure from a sequence of
-// bytes. The input data does not include option code and length bytes.
-func ParseOptStatusCode(data []byte) (*OptStatusCode, error) {
- var opt OptStatusCode
+// FromBytes builds an OptStatusCode structure from a sequence of bytes. The
+// input data does not include option code and length bytes.
+func (op *OptStatusCode) FromBytes(data []byte) error {
buf := uio.NewBigEndianBuffer(data)
- opt.StatusCode = iana.StatusCode(buf.Read16())
- opt.StatusMessage = string(buf.ReadAll())
- return &opt, buf.FinError()
+ op.StatusCode = iana.StatusCode(buf.Read16())
+ op.StatusMessage = string(buf.ReadAll())
+ return buf.FinError()
}
diff --git a/dhcpv6/option_statuscode_test.go b/dhcpv6/option_statuscode_test.go
index 1478678..a2655b2 100644
--- a/dhcpv6/option_statuscode_test.go
+++ b/dhcpv6/option_statuscode_test.go
@@ -1,39 +1,91 @@
package dhcpv6
import (
+ "errors"
+ "fmt"
+ "reflect"
"testing"
+ "github.com/google/go-cmp/cmp"
"github.com/insomniacslk/dhcp/iana"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestParseOptStatusCode(t *testing.T) {
- data := []byte{
- 0, 5, // StatusUseMulticast
- 'u', 's', 'e', ' ', 'm', 'u', 'l', 't', 'i', 'c', 'a', 's', 't',
- }
- opt, err := ParseOptStatusCode(data)
- require.NoError(t, err)
- require.Equal(t, iana.StatusUseMulticast, opt.StatusCode)
- require.Equal(t, "use multicast", opt.StatusMessage)
+type optionsWithStatusCode interface {
+ Status() *OptStatusCode
+ ToBytes() []byte
}
-func TestOptStatusCodeToBytes(t *testing.T) {
- expected := []byte{
- 0, 0, // StatusSuccess
- 's', 'u', 'c', 'c', 'e', 's', 's',
- }
- opt := OptStatusCode{
- StatusCode: iana.StatusSuccess,
- StatusMessage: "success",
+type optionsPtr[O any] interface {
+ *O
+ FromBytes([]byte) error
+ Add(o Option)
+}
+
+type testCase struct {
+ buf []byte
+ err error
+ want *OptStatusCode
+}
+
+func testParseStatus[MO optionsWithStatusCode, OA optionsPtr[MO]](t *testing.T, tt testCase) func(t *testing.T) {
+ return func(t *testing.T) {
+ t.Helper()
+ var mo MO
+ if err := OA(&mo).FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.Status(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Status = %v, want %v", got, tt.want)
+ }
+
+ if tt.want != nil {
+ var m MO
+ OA(&m).Add(tt.want)
+ got := m.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
}
- actual := opt.ToBytes()
- require.Equal(t, expected, actual)
}
-func TestOptStatusCodeParseOptStatusCodeTooShort(t *testing.T) {
- _, err := ParseOptStatusCode([]byte{0})
- require.Error(t, err, "ParseOptStatusCode: Expected error on truncated option")
+func TestStatusCodeParseAndGetter(t *testing.T) {
+ for i, tt := range []testCase{
+ {
+ buf: []byte{
+ 0, 13, // StatusCode option
+ 0, 15, // length
+ 0, 5, // StatusUseMulticast
+ 'u', 's', 'e', ' ', 'm', 'u', 'l', 't', 'i', 'c', 'a', 's', 't',
+ },
+ want: &OptStatusCode{
+ StatusCode: iana.StatusUseMulticast,
+ StatusMessage: "use multicast",
+ },
+ },
+ {
+ buf: nil,
+ want: nil,
+ },
+ {
+ buf: []byte{0, 13, 0, 1, 0},
+ want: nil,
+ err: uio.ErrBufferTooShort,
+ },
+ {
+ buf: []byte{0, 13, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("MO-%d", i), testParseStatus[MessageOptions, *MessageOptions](t, tt))
+ t.Run(fmt.Sprintf("IO-%d", i), testParseStatus[IdentityOptions, *IdentityOptions](t, tt))
+ t.Run(fmt.Sprintf("AO-%d", i), testParseStatus[AddressOptions, *AddressOptions](t, tt))
+ t.Run(fmt.Sprintf("PDO-%d", i), testParseStatus[PDOptions, *PDOptions](t, tt))
+ t.Run(fmt.Sprintf("PO-%d", i), testParseStatus[PrefixOptions, *PrefixOptions](t, tt))
+ }
}
func TestOptStatusCodeString(t *testing.T) {
@@ -41,13 +93,14 @@ func TestOptStatusCodeString(t *testing.T) {
0, 5, // StatusUseMulticast
'u', 's', 'e', ' ', 'm', 'u', 'l', 't', 'i', 'c', 'a', 's', 't',
}
- opt, err := ParseOptStatusCode(data)
+ var opt OptStatusCode
+ err := opt.FromBytes(data)
require.NoError(t, err)
require.Contains(
t,
opt.String(),
- "Code: UseMulticast (5); Message: use multicast",
+ "Code=UseMulticast (5); Message=use multicast",
"String() should contain the code and message",
)
}
diff --git a/dhcpv6/option_temporaryaddress.go b/dhcpv6/option_temporaryaddress.go
index f9dd3ca..cc102a4 100644
--- a/dhcpv6/option_temporaryaddress.go
+++ b/dhcpv6/option_temporaryaddress.go
@@ -9,8 +9,8 @@ import (
// OptIATA implements the identity association for non-temporary addresses
// option.
//
-// This module defines the OptIATA structure.
-// https://www.ietf.org/rfc/rfc8415.txt
+// This module defines the OptIATA structure, as defined by RFC 8415 Section
+// 21.5.
type OptIATA struct {
IaId [4]byte
Options IdentityOptions
@@ -30,19 +30,22 @@ func (op *OptIATA) ToBytes() []byte {
}
func (op *OptIATA) String() string {
- return fmt.Sprintf("IATA: {IAID=%v, options=%v}",
- op.IaId, op.Options)
+ return fmt.Sprintf("%s: {IAID=%#x, Options=%v}", op.Code(), op.IaId, op.Options)
}
-// ParseOptIATA builds an OptIATA structure from a sequence of bytes. The
-// input data does not include option code and length bytes.
-func ParseOptIATA(data []byte) (*OptIATA, error) {
- var opt OptIATA
+// LongString returns a multi-line string representation of IATA data.
+func (op *OptIATA) LongString(indentSpace int) string {
+ return fmt.Sprintf("%s: IAID=%#x Options=%v", op.Code(), op.IaId, op.Options.LongString(indentSpace))
+}
+
+// FromBytes builds an OptIATA structure from a sequence of bytes. The input
+// data does not include option code and length bytes.
+func (op *OptIATA) FromBytes(data []byte) error {
buf := uio.NewBigEndianBuffer(data)
- buf.ReadBytes(opt.IaId[:])
+ buf.ReadBytes(op.IaId[:])
- if err := opt.Options.FromBytes(buf.ReadAll()); err != nil {
- return nil, err
+ if err := op.Options.FromBytes(buf.ReadAll()); err != nil {
+ return err
}
- return &opt, buf.FinError()
+ return buf.FinError()
}
diff --git a/dhcpv6/option_temporaryaddress_test.go b/dhcpv6/option_temporaryaddress_test.go
index de7fe39..693ffa6 100644
--- a/dhcpv6/option_temporaryaddress_test.go
+++ b/dhcpv6/option_temporaryaddress_test.go
@@ -1,38 +1,142 @@
package dhcpv6
import (
+ "errors"
+ "fmt"
"net"
+ "reflect"
"testing"
"time"
+ "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestOptIATAParseOptIATA(t *testing.T) {
- data := []byte{
- 1, 0, 0, 0, // IAID
- 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0xb2, 0x7a, 0, 0, 0xc0, 0x8a, // options
- }
- opt, err := ParseOptIATA(data)
- require.NoError(t, err)
- require.Equal(t, OptionIATA, opt.Code())
-}
+func TestIATAParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want []*OptIATA
+ }{
+ {
+ buf: []byte{
+ 0, 4, // IATA option code
+ 0, 32, // length
+ 1, 0, 0, 0, // IAID
+ 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, // IPv6
+ 0, 0, 0, 2, // PreferredLifetime
+ 0, 0, 0, 4, // ValidLifetime
+ },
+ want: []*OptIATA{
+ &OptIATA{
+ IaId: [4]byte{1, 0, 0, 0},
+ Options: IdentityOptions{Options: Options{&OptIAAddress{
+ IPv6Addr: net.IP{0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0},
+ PreferredLifetime: 2 * time.Second,
+ ValidLifetime: 4 * time.Second,
+ Options: AddressOptions{Options: Options{}},
+ }}},
+ },
+ },
+ },
+ {
+ buf: []byte{
+ 0, 4, // IATA option code
+ 0, 32, // length
+ 1, 0, 0, 0, // IAID
+ 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, // IPv6
+ 0, 0, 0, 2, // PreferredLifetime
+ 0, 0, 0, 4, // ValidLifetime
-func TestOptIATAParseOptIATAInvalidLength(t *testing.T) {
- data := []byte{
- 1, 0, 0, // truncated IAID
- }
- _, err := ParseOptIATA(data)
- require.Error(t, err)
-}
+ 0, 4, // IATA option code
+ 0, 32, // length
+ 1, 2, 3, 4, // IAID
+ 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, // IPv6
+ 0, 0, 0, 2, // PreferredLifetime
+ 0, 0, 0, 4, // ValidLifetime
+ },
+ want: []*OptIATA{
+ &OptIATA{
+ IaId: [4]byte{1, 0, 0, 0},
+ Options: IdentityOptions{Options: Options{&OptIAAddress{
+ IPv6Addr: net.IP{0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0},
+ PreferredLifetime: 2 * time.Second,
+ ValidLifetime: 4 * time.Second,
+ Options: AddressOptions{Options: Options{}},
+ }}},
+ },
+ &OptIATA{
+ IaId: [4]byte{1, 2, 3, 4},
+ Options: IdentityOptions{Options: Options{&OptIAAddress{
+ IPv6Addr: net.IP{0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0},
+ PreferredLifetime: 2 * time.Second,
+ ValidLifetime: 4 * time.Second,
+ Options: AddressOptions{Options: Options{}},
+ }}},
+ },
+ },
+ },
-func TestOptIATAParseOptIATAInvalidOptions(t *testing.T) {
- data := []byte{
- 1, 0, 0, 0, // IAID
- 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0xb2, 0x7a, // truncated options
+ {
+ buf: nil,
+ want: nil,
+ },
+ {
+ buf: []byte{0, 4, 0, 1, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ {
+ buf: []byte{
+ 0, 4, // IATA option code
+ 0, 3, // length
+ 1, 0, 0, // IAID too short
+ },
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ {
+ buf: []byte{
+ 0, 4, // IATA option code
+ 0, 28, // length
+ 1, 0, 0, 0, // IAID
+ 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, // IPv6
+ 0, 0, 0xb2, 0x7a, // PreferredLifetime
+ // Missing ValidLifetime
+ },
+ want: nil,
+ err: uio.ErrBufferTooShort,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.IATA(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("IATA = %v, want %v", got, tt.want)
+ }
+ var wantOne *OptIATA
+ if len(tt.want) >= 1 {
+ wantOne = tt.want[0]
+ }
+ if got := mo.OneIATA(); !reflect.DeepEqual(got, wantOne) {
+ t.Errorf("OneIATA = %v, want %v", got, wantOne)
+ }
+
+ if len(tt.want) >= 1 {
+ var b MessageOptions
+ for _, iata := range tt.want {
+ b.Add(iata)
+ }
+ got := b.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
}
- _, err := ParseOptIATA(data)
- require.Error(t, err)
}
func TestOptIATAGetOneOption(t *testing.T) {
@@ -87,37 +191,24 @@ func TestOptIATADelOption(t *testing.T) {
require.Equal(t, iana2.Options.Options, Options{&optsc})
}
-func TestOptIATAToBytes(t *testing.T) {
- opt := OptIATA{
- IaId: [4]byte{1, 2, 3, 4},
- Options: IdentityOptions{[]Option{
- OptElapsedTime(10 * time.Millisecond),
- }},
- }
- expected := []byte{
- 1, 2, 3, 4, // IA ID
- 0, 8, 0, 2, 0x00, 0x01,
- }
- require.Equal(t, expected, opt.ToBytes())
-}
-
func TestOptIATAString(t *testing.T) {
data := []byte{
1, 0, 0, 0, // IAID
0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0xb2, 0x7a, 0, 0, 0xc0, 0x8a, // options
}
- opt, err := ParseOptIATA(data)
+ var opt OptIATA
+ err := opt.FromBytes(data)
require.NoError(t, err)
str := opt.String()
require.Contains(
t, str,
- "IAID=[1 0 0 0]",
+ "IAID=0x01000000",
"String() should return the IAID",
)
require.Contains(
t, str,
- "options={",
+ "Options={",
"String() should return a list of options",
)
}
diff --git a/dhcpv6/option_userclass.go b/dhcpv6/option_userclass.go
index e5d7b31..8eeb3d7 100644
--- a/dhcpv6/option_userclass.go
+++ b/dhcpv6/option_userclass.go
@@ -35,20 +35,19 @@ func (op *OptUserClass) String() string {
for _, uc := range op.UserClasses {
ucStrings = append(ucStrings, string(uc))
}
- return fmt.Sprintf("OptUserClass{userclass=[%s]}", strings.Join(ucStrings, ", "))
+ return fmt.Sprintf("%s: [%s]", op.Code(), strings.Join(ucStrings, ", "))
}
-// ParseOptUserClass builds an OptUserClass structure from a sequence of
-// bytes. The input data does not include option code and length bytes.
-func ParseOptUserClass(data []byte) (*OptUserClass, error) {
- var opt OptUserClass
+// FromBytes builds an OptUserClass structure from a sequence of bytes. The
+// input data does not include option code and length bytes.
+func (op *OptUserClass) FromBytes(data []byte) error {
if len(data) == 0 {
- return nil, fmt.Errorf("user class option must not be empty")
+ return fmt.Errorf("%w: user class option must not be empty", uio.ErrBufferTooShort)
}
buf := uio.NewBigEndianBuffer(data)
for buf.Has(2) {
len := buf.Read16()
- opt.UserClasses = append(opt.UserClasses, buf.CopyN(int(len)))
+ op.UserClasses = append(op.UserClasses, buf.CopyN(int(len)))
}
- return &opt, buf.FinError()
+ return buf.FinError()
}
diff --git a/dhcpv6/option_userclass_test.go b/dhcpv6/option_userclass_test.go
index 16fae72..ff2926b 100644
--- a/dhcpv6/option_userclass_test.go
+++ b/dhcpv6/option_userclass_test.go
@@ -1,79 +1,64 @@
package dhcpv6
import (
+ "errors"
+ "fmt"
+ "reflect"
"testing"
+ "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestParseOptUserClass(t *testing.T) {
- expected := []byte{
- 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
- }
- opt, err := ParseOptUserClass(expected)
- require.NoError(t, err)
- require.Equal(t, 1, len(opt.UserClasses))
- require.Equal(t, []byte("linuxboot"), opt.UserClasses[0])
-}
-
-func TestParseOptUserClassMultiple(t *testing.T) {
- expected := []byte{
- 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
- 0, 4, 't', 'e', 's', 't',
- }
- opt, err := ParseOptUserClass(expected)
- require.NoError(t, err)
- require.Equal(t, len(opt.UserClasses), 2)
- require.Equal(t, []byte("linuxboot"), opt.UserClasses[0])
- require.Equal(t, []byte("test"), opt.UserClasses[1])
-}
-
-func TestParseOptUserClassNone(t *testing.T) {
- expected := []byte{}
- _, err := ParseOptUserClass(expected)
- require.Error(t, err)
-}
-
-func TestOptUserClassToBytes(t *testing.T) {
- opt := OptUserClass{
- UserClasses: [][]byte{[]byte("linuxboot")},
- }
- data := opt.ToBytes()
- expected := []byte{
- 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
- }
- require.Equal(t, expected, data)
-}
-
-func TestOptUserClassToBytesMultiple(t *testing.T) {
- opt := OptUserClass{
- UserClasses: [][]byte{
- []byte("linuxboot"),
- []byte("test"),
+func TestUserClassParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want [][]byte
+ }{
+ {
+ buf: joinBytes([]byte{
+ 0, 15, // User Class
+ 0, 19, // length
+ 0, 8,
+ }, []byte("bladibla"), []byte{0, 7}, []byte("foo=bar")),
+ want: [][]byte{[]byte("bladibla"), []byte("foo=bar")},
},
- }
- data := opt.ToBytes()
- expected := []byte{
- 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
- 0, 4, 't', 'e', 's', 't',
- }
- require.Equal(t, expected, data)
-}
-
-func TestOptUserClassParseOptUserClassTooShort(t *testing.T) {
- buf := []byte{
- 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
- 0, 4, 't', 'e',
- }
- _, err := ParseOptUserClass(buf)
- require.Error(t, err, "ParseOptUserClass() should error if given truncated user classes")
+ {
+ buf: nil,
+ },
+ {
+ buf: []byte{
+ 0, 15,
+ 0, 0,
+ },
+ err: uio.ErrBufferTooShort,
+ },
+ {
+ buf: []byte{0, 15, 0},
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.UserClasses(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("UserClass = %v, want %v", got, tt.want)
+ }
- buf = []byte{
- 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
- 0,
+ if tt.want != nil {
+ var m MessageOptions
+ m.Add(&OptUserClass{UserClasses: tt.want})
+ got := m.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
}
- _, err = ParseOptUserClass(buf)
- require.Error(t, err, "ParseOptUserClass() should error if given a truncated length")
}
func TestOptUserClassString(t *testing.T) {
@@ -81,13 +66,14 @@ func TestOptUserClassString(t *testing.T) {
0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
0, 4, 't', 'e', 's', 't',
}
- opt, err := ParseOptUserClass(data)
+ var opt OptUserClass
+ err := opt.FromBytes(data)
require.NoError(t, err)
require.Contains(
t,
opt.String(),
- "userclass=[linuxboot, test]",
+ "User Class: [linuxboot, test]",
"String() should contain the list of user classes",
)
}
diff --git a/dhcpv6/option_vendor_opts.go b/dhcpv6/option_vendor_opts.go
index 7965908..8412fd9 100644
--- a/dhcpv6/option_vendor_opts.go
+++ b/dhcpv6/option_vendor_opts.go
@@ -30,21 +30,23 @@ func (op *OptVendorOpts) ToBytes() []byte {
// String returns a string representation of the VendorOpts data
func (op *OptVendorOpts) String() string {
- return fmt.Sprintf("OptVendorOpts{enterprisenum=%v, vendorOpts=%v}",
- op.EnterpriseNumber, op.VendorOpts,
- )
+ return fmt.Sprintf("%s: {EnterpriseNumber=%v VendorOptions=%v}", op.Code(), op.EnterpriseNumber, op.VendorOpts)
}
-// ParseOptVendorOpts builds an OptVendorOpts structure from a sequence of bytes.
-// The input data does not include option code and length bytes.
-func ParseOptVendorOpts(data []byte) (*OptVendorOpts, error) {
- var opt OptVendorOpts
+// LongString returns a string representation of the VendorOpts data
+func (op *OptVendorOpts) LongString(indent int) string {
+ return fmt.Sprintf("%s: EnterpriseNumber=%v VendorOptions=%s", op.Code(), op.EnterpriseNumber, op.VendorOpts.LongString(indent))
+}
+
+// FromBytes builds an OptVendorOpts structure from a sequence of bytes. The
+// input data does not include option code and length bytes.
+func (op *OptVendorOpts) FromBytes(data []byte) error {
buf := uio.NewBigEndianBuffer(data)
- opt.EnterpriseNumber = buf.Read32()
- if err := opt.VendorOpts.FromBytesWithParser(buf.ReadAll(), vendParseOption); err != nil {
- return nil, err
+ op.EnterpriseNumber = buf.Read32()
+ if err := op.VendorOpts.FromBytesWithParser(buf.ReadAll(), vendParseOption); err != nil {
+ return err
}
- return &opt, buf.FinError()
+ return buf.FinError()
}
// vendParseOption builds a GenericOption from a slice of bytes
diff --git a/dhcpv6/option_vendor_opts_test.go b/dhcpv6/option_vendor_opts_test.go
index f6c2b2a..569b45e 100644
--- a/dhcpv6/option_vendor_opts_test.go
+++ b/dhcpv6/option_vendor_opts_test.go
@@ -1,47 +1,94 @@
package dhcpv6
import (
+ "errors"
+ "fmt"
+ "reflect"
"testing"
- "github.com/stretchr/testify/require"
+ "github.com/google/go-cmp/cmp"
+ "github.com/u-root/uio/uio"
)
-func TestOptVendorOpts(t *testing.T) {
- optData := []byte("Arista;DCS-7304;01.00;HSH14425148")
- // NOTE: this should be aware of endianness
- expected := []byte{0xaa, 0xbb, 0xcc, 0xdd}
- expected = append(expected, []byte{0, 1, //code
- 0, byte(len(optData)), //length
- }...)
- expected = append(expected, optData...)
- expectedOpts := OptVendorOpts{}
- var vendorOpts []Option
- expectedOpts.VendorOpts = append(vendorOpts, &OptionGeneric{OptionCode: 1, OptionData: optData})
- opt, err := ParseOptVendorOpts(expected)
- require.NoError(t, err)
- require.Equal(t, uint32(0xaabbccdd), opt.EnterpriseNumber)
- require.Equal(t, expectedOpts.VendorOpts, opt.VendorOpts)
+func TestVendorOptsParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want []*OptVendorOpts
+ }{
+ {
+ buf: []byte{
+ 0, 17, // VendorOpts option
+ 0, 10, // length
+ 0, 0, 0, 16,
+ 0, 5, // type
+ 0, 2, // length
+ 0xa, 0xb,
- shortData := make([]byte, 1)
- _, err = ParseOptVendorOpts(shortData)
- require.Error(t, err)
-}
-
-func TestOptVendorOptsToBytes(t *testing.T) {
- optData := []byte("Arista;DCS-7304;01.00;HSH14425148")
- var opts []Option
- opts = append(opts, &OptionGeneric{OptionCode: 1, OptionData: optData})
-
- expected := append([]byte{
- 0, 0, 0, 0, // EnterpriseNumber
- 0, 1, // Sub-Option code from vendor
- 0, byte(len(optData)), // Length of optionData only
- }, optData...)
+ 0, 17, // VendorOpts option
+ 0, 9, // length
+ 0, 0, 0, 14,
+ 0, 9, // type
+ 0, 1, // length
+ 0xa,
+ },
+ want: []*OptVendorOpts{
+ &OptVendorOpts{
+ EnterpriseNumber: 16,
+ VendorOpts: Options{
+ &OptionGeneric{OptionCode: 5, OptionData: []byte{0xa, 0xb}},
+ },
+ },
+ &OptVendorOpts{
+ EnterpriseNumber: 14,
+ VendorOpts: Options{
+ &OptionGeneric{OptionCode: 9, OptionData: []byte{0xa}},
+ },
+ },
+ },
+ },
+ {
+ buf: nil,
+ want: nil,
+ },
+ {
+ buf: []byte{0, 17, 0, 1, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ {
+ buf: []byte{0, 17, 0},
+ want: nil,
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.VendorOpts(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("VendorOpts = %v, want %v", got, tt.want)
+ }
+ for _, vo := range tt.want {
+ if got := mo.VendorOpt(vo.EnterpriseNumber); !reflect.DeepEqual(got, vo.VendorOpts) {
+ t.Errorf("VendorOpt(%d) = %v, want %v", vo.EnterpriseNumber, got, vo.VendorOpts)
+ }
+ }
+ if got := mo.VendorOpt(100); got != nil {
+ t.Errorf("VendorOpt(100) = %v, not nil", got)
+ }
- opt := OptVendorOpts{
- EnterpriseNumber: 0000,
- VendorOpts: opts,
+ if tt.want != nil {
+ var m MessageOptions
+ for _, opt := range tt.want {
+ m.Add(opt)
+ }
+ got := m.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
}
- toBytes := opt.ToBytes()
- require.Equal(t, expected, toBytes)
}
diff --git a/dhcpv6/option_vendorclass.go b/dhcpv6/option_vendorclass.go
index 1144974..f85795e 100644
--- a/dhcpv6/option_vendorclass.go
+++ b/dhcpv6/option_vendorclass.go
@@ -1,7 +1,6 @@
package dhcpv6
import (
- "errors"
"fmt"
"strings"
@@ -36,21 +35,21 @@ func (op *OptVendorClass) String() string {
for _, data := range op.Data {
vcStrings = append(vcStrings, string(data))
}
- return fmt.Sprintf("OptVendorClass{enterprisenum=%d, data=[%s]}", op.EnterpriseNumber, strings.Join(vcStrings, ", "))
+ return fmt.Sprintf("%s: {EnterpriseNumber=%d Data=[%s]}", op.Code(), op.EnterpriseNumber, strings.Join(vcStrings, ", "))
}
-// ParseOptVendorClass builds an OptVendorClass structure from a sequence of
-// bytes. The input data does not include option code and length bytes.
-func ParseOptVendorClass(data []byte) (*OptVendorClass, error) {
- var opt OptVendorClass
+// FromBytes builds an OptVendorClass structure from a sequence of bytes. The
+// input data does not include option code and length bytes.
+func (op *OptVendorClass) FromBytes(data []byte) error {
buf := uio.NewBigEndianBuffer(data)
- opt.EnterpriseNumber = buf.Read32()
+ *op = OptVendorClass{}
+ op.EnterpriseNumber = buf.Read32()
for buf.Has(2) {
len := buf.Read16()
- opt.Data = append(opt.Data, buf.CopyN(int(len)))
+ op.Data = append(op.Data, buf.CopyN(int(len)))
}
- if len(opt.Data) < 1 {
- return nil, errors.New("ParseOptVendorClass: at least one vendor class data is required")
+ if len(op.Data) == 0 {
+ return fmt.Errorf("%w: vendor class data should not be empty", uio.ErrBufferTooShort)
}
- return &opt, buf.FinError()
+ return buf.FinError()
}
diff --git a/dhcpv6/option_vendorclass_test.go b/dhcpv6/option_vendorclass_test.go
index 685b8b0..5fbc0e6 100644
--- a/dhcpv6/option_vendorclass_test.go
+++ b/dhcpv6/option_vendorclass_test.go
@@ -1,71 +1,88 @@
package dhcpv6
import (
+ "errors"
+ "fmt"
+ "reflect"
"testing"
+ "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
+ "github.com/u-root/uio/uio"
)
-func TestParseOptVendorClass(t *testing.T) {
- data := []byte{
- 0xaa, 0xbb, 0xcc, 0xdd, // EnterpriseNumber
- 0, 10, 'H', 'T', 'T', 'P', 'C', 'l', 'i', 'e', 'n', 't',
- 0, 4, 't', 'e', 's', 't',
- }
- opt, err := ParseOptVendorClass(data)
- require.NoError(t, err)
- require.Equal(t, OptionVendorClass, opt.Code())
- require.Equal(t, 2, len(opt.Data))
- require.Equal(t, uint32(0xaabbccdd), opt.EnterpriseNumber)
- require.Equal(t, []byte("HTTPClient"), opt.Data[0])
- require.Equal(t, []byte("test"), opt.Data[1])
-}
-
-func TestOptVendorClassToBytes(t *testing.T) {
- opt := OptVendorClass{
- EnterpriseNumber: uint32(0xaabbccdd),
- Data: [][]byte{
- []byte("HTTPClient"),
- []byte("test"),
+func TestVendorClassParseAndGetter(t *testing.T) {
+ for i, tt := range []struct {
+ buf []byte
+ err error
+ want []*OptVendorClass
+ }{
+ {
+ buf: []byte{
+ 0, 16, // Vendor Class
+ 0, 14, // length
+ 0, 0, 0, 16,
+ 0, 4,
+ 'S', 'L', 'A', 'M',
+ 0, 2,
+ 'h', 'h',
+ },
+ want: []*OptVendorClass{
+ &OptVendorClass{
+ EnterpriseNumber: 16,
+ Data: [][]byte{[]byte("SLAM"), []byte("hh")},
+ },
+ },
},
- }
- data := opt.ToBytes()
- expected := []byte{
- 0xaa, 0xbb, 0xcc, 0xdd, // EnterpriseNumber
- 0, 10, 'H', 'T', 'T', 'P', 'C', 'l', 'i', 'e', 'n', 't',
- 0, 4, 't', 'e', 's', 't',
- }
- require.Equal(t, expected, data)
-}
-
-func TestOptVendorClassParseOptVendorClassMalformed(t *testing.T) {
- buf := []byte{
- 0xaa, 0xbb, // truncated EnterpriseNumber
- }
- _, err := ParseOptVendorClass(buf)
- require.Error(t, err, "ParseOptVendorClass() should error if given truncated EnterpriseNumber")
-
- buf = []byte{
- 0xaa, 0xbb, 0xcc, 0xdd, // EnterpriseNumber
- }
- _, err = ParseOptVendorClass(buf)
- require.Error(t, err, "ParseOptVendorClass() should error if given no vendor classes")
-
- buf = []byte{
- 0xaa, 0xbb, 0xcc, 0xdd, // EnterpriseNumber
- 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
- 0, 4, 't', 'e',
- }
- _, err = ParseOptVendorClass(buf)
- require.Error(t, err, "ParseOptVendorClass() should error if given truncated vendor classes")
+ {
+ buf: []byte{
+ 0, 16,
+ 0, 0,
+ },
+ err: uio.ErrBufferTooShort,
+ },
+ {
+ buf: []byte{
+ 0, 16,
+ 0, 4,
+ 0, 0, 0, 6,
+ },
+ err: uio.ErrBufferTooShort,
+ },
+ {
+ buf: []byte{0, 16, 0},
+ err: uio.ErrUnreadBytes,
+ },
+ } {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ var mo MessageOptions
+ if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) {
+ t.Errorf("FromBytes = %v, want %v", err, tt.err)
+ }
+ if got := mo.VendorClasses(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("VendorClass = %v, want %v", got, tt.want)
+ }
+ for _, v := range tt.want {
+ if got := mo.VendorClass(v.EnterpriseNumber); !reflect.DeepEqual(got, v.Data) {
+ t.Errorf("VendorClass(%d) = %v, want %v", v.EnterpriseNumber, got, v.Data)
+ }
+ }
+ if got := mo.VendorClass(100); got != nil {
+ t.Errorf("VendorClass(100) = %v, want nil", got)
+ }
- buf = []byte{
- 0xaa, 0xbb, 0xcc, 0xdd, // EnterpriseNumber
- 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
- 0,
+ if tt.want != nil {
+ var m MessageOptions
+ for _, o := range tt.want {
+ m.Add(o)
+ }
+ got := m.ToBytes()
+ if diff := cmp.Diff(tt.buf, got); diff != "" {
+ t.Errorf("ToBytes mismatch (-want, +got): %s", diff)
+ }
+ }
+ })
}
- _, err = ParseOptVendorClass(buf)
- require.Error(t, err, "ParseOptVendorClass() should error if given a truncated length")
}
func TestOptVendorClassString(t *testing.T) {
@@ -74,20 +91,21 @@ func TestOptVendorClassString(t *testing.T) {
0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
0, 4, 't', 'e', 's', 't',
}
- opt, err := ParseOptVendorClass(data)
+ var opt OptVendorClass
+ err := opt.FromBytes(data)
require.NoError(t, err)
str := opt.String()
require.Contains(
t,
str,
- "enterprisenum=2864434397",
+ "EnterpriseNumber=2864434397",
"String() should contain the enterprisenum",
)
require.Contains(
t,
str,
- "data=[linuxboot, test]",
+ "Data=[linuxboot, test]",
"String() should contain the list of vendor classes",
)
}
diff --git a/dhcpv6/options.go b/dhcpv6/options.go
index f8445bb..552abd0 100644
--- a/dhcpv6/options.go
+++ b/dhcpv6/options.go
@@ -2,6 +2,7 @@ package dhcpv6
import (
"fmt"
+ "strings"
"github.com/u-root/uio/uio"
)
@@ -11,6 +12,7 @@ type Option interface {
Code() OptionCode
ToBytes() []byte
String() string
+ FromBytes([]byte) error
}
type OptionGeneric struct {
@@ -27,98 +29,126 @@ func (og *OptionGeneric) ToBytes() []byte {
}
func (og *OptionGeneric) String() string {
- return fmt.Sprintf("%s -> %v", og.OptionCode, og.OptionData)
+ if len(og.OptionData) == 0 {
+ return og.OptionCode.String()
+ }
+ return fmt.Sprintf("%s: %v", og.OptionCode, og.OptionData)
+}
+
+// FromBytes resets OptionData to p.
+func (og *OptionGeneric) FromBytes(p []byte) error {
+ og.OptionData = append([]byte(nil), p...)
+ return nil
}
// ParseOption parses data according to the given code.
+//
+// Parse a sequence of bytes as a single DHCPv6 option.
+// Returns the option structure, or an error if any.
func ParseOption(code OptionCode, optData []byte) (Option, error) {
- // Parse a sequence of bytes as a single DHCPv6 option.
- // Returns the option structure, or an error if any.
- var (
- err error
- opt Option
- )
+ var opt Option
switch code {
case OptionClientID:
- opt, err = parseOptClientID(optData)
+ opt = &optClientID{}
case OptionServerID:
- opt, err = parseOptServerID(optData)
+ opt = &optServerID{}
case OptionIANA:
- opt, err = ParseOptIANA(optData)
+ opt = &OptIANA{}
case OptionIATA:
- opt, err = ParseOptIATA(optData)
+ opt = &OptIATA{}
case OptionIAAddr:
- opt, err = ParseOptIAAddress(optData)
+ opt = &OptIAAddress{}
case OptionORO:
- var o optRequestedOption
- err = o.FromBytes(optData)
- opt = &o
+ opt = &optRequestedOption{}
case OptionElapsedTime:
- opt, err = parseOptElapsedTime(optData)
+ opt = &optElapsedTime{}
case OptionRelayMsg:
- opt, err = parseOptRelayMsg(optData)
+ opt = &optRelayMsg{}
case OptionStatusCode:
- opt, err = ParseOptStatusCode(optData)
+ opt = &OptStatusCode{}
case OptionUserClass:
- opt, err = ParseOptUserClass(optData)
+ opt = &OptUserClass{}
case OptionVendorClass:
- opt, err = ParseOptVendorClass(optData)
+ opt = &OptVendorClass{}
case OptionVendorOpts:
- opt, err = ParseOptVendorOpts(optData)
+ opt = &OptVendorOpts{}
case OptionInterfaceID:
- opt, err = parseOptInterfaceID(optData)
+ opt = &optInterfaceID{}
case OptionDNSRecursiveNameServer:
- opt, err = parseOptDNS(optData)
+ opt = &optDNS{}
case OptionDomainSearchList:
- opt, err = parseOptDomainSearchList(optData)
+ opt = &optDomainSearchList{}
case OptionIAPD:
- opt, err = ParseOptIAPD(optData)
+ opt = &OptIAPD{}
case OptionIAPrefix:
- opt, err = ParseOptIAPrefix(optData)
+ opt = &OptIAPrefix{}
case OptionInformationRefreshTime:
- opt, err = parseOptInformationRefreshTime(optData)
+ opt = &optInformationRefreshTime{}
case OptionRemoteID:
- opt, err = ParseOptRemoteID(optData)
+ opt = &OptRemoteID{}
case OptionFQDN:
- opt, err = ParseOptFQDN(optData)
+ opt = &OptFQDN{}
case OptionNTPServer:
- opt, err = ParseOptNTPServer(optData)
+ opt = &OptNTPServer{}
case OptionBootfileURL:
- opt, err = parseOptBootFileURL(optData)
+ opt = &optBootFileURL{}
case OptionBootfileParam:
- opt, err = parseOptBootFileParam(optData)
+ opt = &optBootFileParam{}
case OptionClientArchType:
- opt, err = parseOptClientArchType(optData)
+ opt = &optClientArchType{}
case OptionNII:
- var o OptNetworkInterfaceID
- err = o.FromBytes(optData)
- opt = &o
+ opt = &OptNetworkInterfaceID{}
case OptionClientLinkLayerAddr:
- opt, err = parseOptClientLinkLayerAddress(optData)
+ opt = &optClientLinkLayerAddress{}
case OptionDHCPv4Msg:
- opt, err = ParseOptDHCPv4Msg(optData)
+ opt = &OptDHCPv4Msg{}
case OptionDHCP4oDHCP6Server:
- opt, err = ParseOptDHCP4oDHCP6Server(optData)
+ opt = &OptDHCP4oDHCP6Server{}
case Option4RD:
- opt, err = ParseOpt4RD(optData)
+ opt = &Opt4RD{}
case Option4RDMapRule:
- opt, err = ParseOpt4RDMapRule(optData)
+ opt = &Opt4RDMapRule{}
case Option4RDNonMapRule:
- opt, err = ParseOpt4RDNonMapRule(optData)
+ opt = &Opt4RDNonMapRule{}
case OptionRelayPort:
- opt, err = parseOptRelayPort(optData)
+ opt = &optRelayPort{}
default:
- opt = &OptionGeneric{OptionCode: code, OptionData: optData}
- }
- if err != nil {
- return nil, err
+ opt = &OptionGeneric{OptionCode: code}
}
- return opt, nil
+ return opt, opt.FromBytes(optData)
+}
+
+type longStringer interface {
+ LongString(spaceIndent int) string
}
// Options is a collection of options.
type Options []Option
+// LongString prints options with indentation of at least spaceIndent spaces.
+func (o Options) LongString(spaceIndent int) string {
+ indent := strings.Repeat(" ", spaceIndent)
+ var s strings.Builder
+ if len(o) == 0 {
+ s.WriteString("[]")
+ } else {
+ s.WriteString("[\n")
+ for _, opt := range o {
+ s.WriteString(indent)
+ s.WriteString(" ")
+ if ls, ok := opt.(longStringer); ok {
+ s.WriteString(ls.LongString(spaceIndent + 2))
+ } else {
+ s.WriteString(opt.String())
+ }
+ s.WriteString("\n")
+ }
+ s.WriteString(indent)
+ s.WriteString("]")
+ }
+ return s.String()
+}
+
// Get returns all options matching the option code.
func (o Options) Get(code OptionCode) []Option {
var ret []Option
@@ -194,7 +224,9 @@ type OptionParser func(code OptionCode, data []byte) (Option, error)
// FromBytesWithParser parses Options from byte sequences using the parsing
// function that is passed in as a paremeter
func (o *Options) FromBytesWithParser(data []byte, parser OptionParser) error {
- *o = make(Options, 0, 10)
+ if *o == nil {
+ *o = make(Options, 0, 10)
+ }
if len(data) == 0 {
// no options, no party
return nil
diff --git a/dhcpv6/prettyprint_test.go b/dhcpv6/prettyprint_test.go
new file mode 100644
index 0000000..def212c
--- /dev/null
+++ b/dhcpv6/prettyprint_test.go
@@ -0,0 +1,96 @@
+package dhcpv6
+
+import (
+ "net"
+ "testing"
+ "time"
+
+ "github.com/insomniacslk/dhcp/dhcpv4"
+ "github.com/insomniacslk/dhcp/iana"
+)
+
+func TestPrint(t *testing.T) {
+ m4, _ := dhcpv4.NewDiscovery(net.HardwareAddr{0x1, 0x2, 0xde, 0xad, 0xbe, 0xef})
+
+ m, _ := NewSolicit(net.HardwareAddr{0x1, 0x2, 0xde, 0xad, 0xbe, 0xef}, WithRapidCommit)
+
+ oneiana := m.Options.OneIANA()
+ iaaddr := &OptIAAddress{IPv6Addr: net.ParseIP("fe80::1")}
+ iaaddr.Options.Add(&OptStatusCode{StatusCode: iana.StatusSuccess, StatusMessage: "yes"})
+ oneiana.Options.Add(iaaddr)
+
+ oneiata := &OptIATA{}
+ oneiata.Options.Add(iaaddr)
+
+ fourrd := &Opt4RD{}
+ fourrd.Add(&Opt4RDMapRule{
+ Prefix4: net.IPNet{
+ IP: net.IP{123, 123, 0, 0},
+ Mask: net.CIDRMask(16, 32),
+ },
+ Prefix6: net.IPNet{
+ IP: net.ParseIP("fc80::"),
+ Mask: net.CIDRMask(64, 128),
+ },
+ })
+ fourrd.Add(&Opt4RDNonMapRule{
+ HubAndSpoke: true,
+ })
+
+ iapd := &OptIAPD{
+ IaId: [4]byte{0x1, 0x2, 0x3, 0x4},
+ }
+ iaprefix := &OptIAPrefix{
+ Prefix: &net.IPNet{
+ IP: net.ParseIP("fc80::"),
+ Mask: net.CIDRMask(64, 128),
+ },
+ }
+ iaprefix.Options.Add(&OptStatusCode{StatusCode: iana.StatusSuccess, StatusMessage: "yeah whatever"})
+ iapd.Options.Add(iaprefix)
+
+ vendorOpts := &OptVendorOpts{
+ EnterpriseNumber: 123,
+ }
+ vendorOpts.VendorOpts.Add(&OptionGeneric{OptionCode: 400, OptionData: []byte("foobar")})
+
+ adv, _ := NewReplyFromMessage(m,
+ WithOption(OptClientArchType(iana.INTEL_X86PC, iana.EFI_X86_64)),
+ WithOption(OptBootFileURL("http://foobar")),
+ WithOption(OptBootFileParam("loglevel=10", "uroot.nohwrng")),
+ WithOption(OptClientLinkLayerAddress(iana.HWTypeEthernet, net.HardwareAddr{0x1, 0x2, 0xbe, 0xef, 0xde, 0xad})),
+ WithOption(fourrd),
+ WithOption(&OptDHCPv4Msg{m4}),
+ WithOption(&OptDHCP4oDHCP6Server{[]net.IP{net.ParseIP("fe81::1")}}),
+ WithOption(OptDNS(net.ParseIP("fe82::1"))),
+ WithOption(iapd),
+ WithOption(OptInformationRefreshTime(1*time.Second)),
+ WithOption(OptInterfaceID([]byte{0x1, 0x2})),
+ WithOption(&OptNetworkInterfaceID{
+ Typ: NII_PXE_GEN_I,
+ Major: 1,
+ }),
+ WithOption(OptRelayPort(1026)),
+ WithOption(&OptRemoteID{EnterpriseNumber: 300, RemoteID: []byte{0xde, 0xad, 0xbe, 0xed}}),
+ WithOption(OptRequestedOption(OptionBootfileURL, OptionBootfileParam)),
+ WithOption(OptServerID(&DUIDLL{HWType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr{0x1, 0x2, 0x3, 0x4, 0x5, 0x6}})),
+ WithOption(&OptUserClass{[][]byte{[]byte("foo"), []byte("bar")}}),
+ WithOption(oneiana),
+ WithOption(oneiata),
+ WithOption(&OptVendorClass{EnterpriseNumber: 300, Data: [][]byte{[]byte("foo"), []byte("bar")}}),
+ WithOption(vendorOpts),
+ )
+ t.Log(adv.String())
+ t.Log(adv.Summary())
+
+ relayfw := RelayMessage{
+ MessageType: MessageTypeRelayForward,
+ }
+ relayfw.Options.Add(OptRelayMessage(adv))
+ relayfw.Options.Add(&OptRemoteID{
+ EnterpriseNumber: 0x123,
+ RemoteID: []byte{0x1, 0x2},
+ })
+ t.Log(relayfw.String())
+ t.Log(relayfw.Summary())
+}
diff --git a/dhcpv6/types.go b/dhcpv6/types.go
index 7c4052f..be5d925 100644
--- a/dhcpv6/types.go
+++ b/dhcpv6/types.go
@@ -234,10 +234,10 @@ const (
// optionCodeToString maps DHCPv6 OptionCodes to human-readable strings.
var optionCodeToString = map[OptionCode]string{
- OptionClientID: "Client Identifier",
- OptionServerID: "Server Identifier",
- OptionIANA: "IA_NA",
- OptionIATA: "IA_TA",
+ OptionClientID: "Client ID",
+ OptionServerID: "Server ID",
+ OptionIANA: "IANA",
+ OptionIATA: "IATA",
OptionIAAddr: "IA IP Address",
OptionORO: "Requested Options",
OptionPreference: "Preference",
@@ -255,9 +255,9 @@ var optionCodeToString = map[OptionCode]string{
OptionReconfAccept: "Reconfig Accept",
OptionSIPServersDomainNameList: "SIP Servers Domain Name List",
OptionSIPServersIPv6AddressList: "SIP Servers IPv6 Address List",
- OptionDNSRecursiveNameServer: "DNS Recursive Name Server",
+ OptionDNSRecursiveNameServer: "DNS",
OptionDomainSearchList: "Domain Search List",
- OptionIAPD: "IA_PD",
+ OptionIAPD: "IAPD",
OptionIAPrefix: "IA Prefix",
OptionNISServers: "NIS Servers",
OptionNISPServers: "NISP Servers",
@@ -284,7 +284,7 @@ var optionCodeToString = map[OptionCode]string{
OptionMIPv6VisitedHomeNetworkInformation: "MIPv6 Visited Home Network Information",
OptionLoSTServer: "LoST Server",
OptionCAPWAPAccessControllerAddresses: "CAPWAP Access Controller Addresses",
- OptionRelayID: "RELAY_ID",
+ OptionRelayID: "Relay ID",
OptionIPv6AddressMOS: "OPTION-IPv6_Address-MoS",
OptionIPv6FQDNMOS: "OPTION-IPv6-FQDN-MoS",
OptionNTPServer: "NTP Server",
@@ -328,9 +328,9 @@ var optionCodeToString = map[OptionCode]string{
OptionS46ContMapE: "Softwire46 MAP-E Container",
OptionS46ContMapT: "Softwire46 MAP-T Container",
OptionS46ContLW: "Softwire46 Lightweight 4over6 Container",
- Option4RD: "IPv4 Residual Deployment",
- Option4RDMapRule: "IPv4 Residual Deployment Mapping Rule",
- Option4RDNonMapRule: "IPv4 Residual Deployment Non-Mapping Rule",
+ Option4RD: "4RD",
+ Option4RDMapRule: "4RD Mapping Rule",
+ Option4RDNonMapRule: "4RD Non-Mapping Rule",
OptionLQBaseTime: "Leasequery Server Base time",
OptionLQStartTime: "Leasequery Server Query Start Time",
OptionLQEndTime: "Leasequery Server Query End Time",
diff --git a/dhcpv6/ztpv6/parse_vendor_options.go b/dhcpv6/ztpv6/parse_vendor_options.go
index b3ce55b..f1508fd 100644
--- a/dhcpv6/ztpv6/parse_vendor_options.go
+++ b/dhcpv6/ztpv6/parse_vendor_options.go
@@ -81,10 +81,12 @@ func ParseVendorData(packet dhcpv6.DHCPv6) (*VendorData, error) {
if len(v) < 3 {
return nil, errVendorOptionMalformed
}
- duid := packet.(*dhcpv6.Message).Options.ClientID()
vd.VendorName = iana.EnterpriseIDCienaCorporation.String()
vd.Model = v[1] + "-" + v[2]
- vd.Serial = string(duid.EnterpriseIdentifier)
+ duid := packet.(*dhcpv6.Message).Options.ClientID()
+ if enterpriseDUID, ok := duid.(*dhcpv6.DUIDEN); ok {
+ vd.Serial = string(enterpriseDUID.EnterpriseIdentifier)
+ }
return &vd, nil
}
}
diff --git a/dhcpv6/ztpv6/parse_vendor_options_test.go b/dhcpv6/ztpv6/parse_vendor_options_test.go
index 03ff8cf..cfbfdde 100644
--- a/dhcpv6/ztpv6/parse_vendor_options_test.go
+++ b/dhcpv6/ztpv6/parse_vendor_options_test.go
@@ -64,7 +64,7 @@ func TestParseVendorDataWithVendorClass(t *testing.T) {
tt := []struct {
name string
vc string
- clientId *dhcpv6.Duid
+ clientId dhcpv6.DUID
want *VendorData
fail bool
}{
@@ -84,8 +84,7 @@ func TestParseVendorDataWithVendorClass(t *testing.T) {
{
name: "Ciena",
vc: "1271-23422Z11-123",
- clientId: &dhcpv6.Duid{
- Type: dhcpv6.DUID_EN,
+ clientId: &dhcpv6.DUIDEN{
EnterpriseIdentifier: []byte("001234567"),
},
want: &VendorData{VendorName: iana.EnterpriseIDCienaCorporation.String(), Model: "23422Z11-123", Serial: "001234567"},
@@ -102,7 +101,7 @@ func TestParseVendorDataWithVendorClass(t *testing.T) {
packet.AddOption(&dhcpv6.OptVendorClass{
EnterpriseNumber: 0000, Data: [][]byte{[]byte(tc.vc)}})
if tc.clientId != nil {
- packet.AddOption(dhcpv6.OptClientID(*tc.clientId))
+ packet.AddOption(dhcpv6.OptClientID(tc.clientId))
}
vd, err := ParseVendorData(packet)
if err != nil && !tc.fail {
diff --git a/examples/packetcrafting6/main.go b/examples/packetcrafting6/main.go
index f2c1855..13e1c22 100644
--- a/examples/packetcrafting6/main.go
+++ b/examples/packetcrafting6/main.go
@@ -41,9 +41,8 @@ func main() {
if err != nil {
log.Fatal(err)
}
- duid := dhcpv6.Duid{
- Type: dhcpv6.DUID_LLT,
- HwType: iana.HWTypeEthernet,
+ duid := &dhcpv6.DUIDLLT{
+ HWType: iana.HWTypeEthernet,
Time: dhcpv6.GetTime(),
LinkLayerAddr: mac,
}
diff --git a/go.mod b/go.mod
index d53b0e4..1f7e78f 100644
--- a/go.mod
+++ b/go.mod
@@ -1,17 +1,28 @@
module github.com/insomniacslk/dhcp
-go 1.13
+go 1.18
require (
github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72
+ github.com/google/go-cmp v0.5.9
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714
github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c
- github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7
github.com/mdlayher/netlink v1.1.1
- github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065
- github.com/smartystreets/goconvey v1.6.4 // indirect
+ github.com/mdlayher/packet v1.1.1
github.com/stretchr/testify v1.6.1
- github.com/u-root/uio v0.0.0-20210528114334-82958018845c
- golang.org/x/net v0.0.0-20201110031124-69a78807bb2b
- golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea
+ github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923
+ golang.org/x/net v0.7.0
+ golang.org/x/sys v0.5.0
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.0 // indirect
+ github.com/josharian/native v1.1.0 // indirect
+ github.com/mdlayher/socket v0.4.0 // indirect
+ github.com/pierrec/lz4/v4 v4.1.14 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/smartystreets/goconvey v1.6.4 // indirect
+ github.com/stretchr/objx v0.1.0 // indirect
+ golang.org/x/sync v0.1.0 // indirect
+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
diff --git a/go.sum b/go.sum
index a89fe9d..058dbef 100644
--- a/go.sum
+++ b/go.sum
@@ -3,15 +3,18 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72 h1:0eU/faU2oDIB2BkQVM02hgRLJjGzzUuRf19HUhp0394=
github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
+github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
+github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
+github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
@@ -19,16 +22,17 @@ github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c h1:7cpGGTQO6+
github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
-github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE=
-github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
github.com/mdlayher/netlink v1.1.1 h1:VqG+Voq9V4uZ+04vjIrcSCWDpf91B1xxbP4QBUmUJE8=
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
-github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
-github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w=
-github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
+github.com/mdlayher/packet v1.1.1 h1:7Fv4OEMYqPl7//uBm04VgPpnSNi8fbBZznppgh6WMr8=
+github.com/mdlayher/packet v1.1.1/go.mod h1:DRvYY5mH4M4lUqAnMg04E60U4fjUKMZ/4g2cHElZkKo=
+github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw=
+github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc=
+github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
+github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
@@ -39,39 +43,37 @@ github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/u-root/uio v0.0.0-20210528114334-82958018845c h1:BFvcl34IGnw8yvJi8hlqLFo9EshRInwWBs2M5fGWzQA=
-github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
+github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA=
+github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
-golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea h1:+WiDlPBBaO+h9vPNZi8uJ3k4BkKQB7Iow3aqwHVA5hI=
-golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/netboot/netboot.go b/netboot/netboot.go
index b32f69e..adfc7f3 100644
--- a/netboot/netboot.go
+++ b/netboot/netboot.go
@@ -48,7 +48,6 @@ func RequestNetbootv6(ifname string, timeout time.Duration, retries int, modifie
if err != nil {
log.Printf("Client.Exchange failed: %v", err)
if i >= retries {
- // don't wait at the end of the last attempt
return nil, fmt.Errorf("netboot failed after %d attempts: %v", retries+1, err)
}
log.Printf("sleeping %v before retrying", delay)
@@ -80,8 +79,7 @@ func RequestNetbootv4(ifname string, timeout time.Duration, retries int, modifie
log.Printf("Client.Exchange failed: %v", err)
log.Printf("sleeping %v before retrying", delay)
if i >= retries {
- // don't wait at the end of the last attempt
- break
+ return nil, fmt.Errorf("netboot failed after %d attempts: %v", retries+1, err)
}
sleeper(delay)
// TODO add random splay
diff --git a/rfc1035label/label.go b/rfc1035label/label.go
index 042c17b..f727ec6 100644
--- a/rfc1035label/label.go
+++ b/rfc1035label/label.go
@@ -89,6 +89,10 @@ func FromBytes(data []byte) (*Labels, error) {
return &l, nil
}
+// ErrBufferTooShort is returned when the label cannot be parsed due to a wrong
+// length or missing bytes.
+var ErrBufferTooShort = errors.New("rfc1035label: buffer too short")
+
// fromBytes decodes a serialized stream and returns a list of labels
func labelsFromBytes(buf []byte) ([]string, error) {
var (
@@ -132,7 +136,7 @@ func labelsFromBytes(buf []byte) ([]string, error) {
pos = off
} else {
if pos+length > len(buf) {
- return nil, errors.New("rfc1035label: buffer too short")
+ return nil, ErrBufferTooShort
}
chunk = string(buf[pos : pos+length])
if label != "" {
More details
Historical runs
- failed: src/github.com/insomniacslk/dhcp/dhcpv6/duid.go:277:79: error: reference to undefined identifier ‘uio.ErrBufferTooShort’
- nothing-to-do: Last upstream version 0.0~git20220915.043f172 already imported.
- success: Merged new upstream version 0.0~git20220504.1ca156e
- success: Merged new upstream version 0.0~git20220405.12fbdcb
- nothing-to-do: Last upstream version 0.0~git20220119.3c283ff already imported. Import a snapshot by specifying --snapshot.
- build-failed-stage-explain-bd-uninstallable: build failed stage explain-bd-uninstallable