diff options
Diffstat (limited to 'dhcpv4')
67 files changed, 2037 insertions, 2604 deletions
diff --git a/dhcpv4/bsdp/boot_image.go b/dhcpv4/bsdp/boot_image.go index 954dcb6..58b5167 100644 --- a/dhcpv4/bsdp/boot_image.go +++ b/dhcpv4/bsdp/boot_image.go @@ -3,6 +3,7 @@ package bsdp import ( "fmt" + "github.com/insomniacslk/dhcp/dhcpv4" "github.com/u-root/u-root/pkg/uio" ) @@ -18,9 +19,9 @@ const ( // 4 - 127 are reserved for future use. ) -// BootImageTypeToString maps the different BootImageTypes to human-readable +// bootImageTypeToString maps the different BootImageTypes to human-readable // representations. -var BootImageTypeToString = map[BootImageType]string{ +var bootImageTypeToString = map[BootImageType]string{ BootImageTypeMacOS9: "macOS 9", BootImageTypeMacOSX: "macOS", BootImageTypeMacOSXServer: "macOS Server", @@ -35,6 +36,16 @@ type BootImageID struct { Index uint16 } +// ToBytes implements dhcpv4.OptionValue. +func (b BootImageID) ToBytes() []byte { + return uio.ToBigEndian(b) +} + +// FromBytes reads data into b. +func (b *BootImageID) FromBytes(data []byte) error { + return uio.FromBigEndian(b, data) +} + // Marshal writes the binary representation to buf. func (b BootImageID) Marshal(buf *uio.Lexer) { var byte0 byte @@ -55,7 +66,7 @@ func (b BootImageID) String() string { } else { s += " uninstallable" } - t, ok := BootImageTypeToString[b.ImageType] + t, ok := bootImageTypeToString[b.ImageType] if !ok { t = "unknown" } @@ -99,3 +110,37 @@ func (b *BootImage) Unmarshal(buf *uio.Lexer) error { b.Name = string(buf.Consume(int(nameLength))) return buf.Error() } + +func getBootImageID(code dhcpv4.OptionCode, o dhcpv4.Options) *BootImageID { + v := o.Get(code) + if v == nil { + return nil + } + var b BootImageID + if err := uio.FromBigEndian(&b, v); err != nil { + return nil + } + return &b +} + +// OptDefaultBootImageID returns a new default boot image ID option as per +// BSDP. +func OptDefaultBootImageID(b BootImageID) dhcpv4.Option { + return dhcpv4.Option{Code: OptionDefaultBootImageID, Value: b} +} + +// GetDefaultBootImageID returns the default boot image ID contained in o. +func GetDefaultBootImageID(o dhcpv4.Options) *BootImageID { + return getBootImageID(OptionDefaultBootImageID, o) +} + +// OptSelectedBootImageID returns a new selected boot image ID option as per +// BSDP. +func OptSelectedBootImageID(b BootImageID) dhcpv4.Option { + return dhcpv4.Option{Code: OptionSelectedBootImageID, Value: b} +} + +// GetSelectedBootImageID returns the selected boot image ID contained in o. +func GetSelectedBootImageID(o dhcpv4.Options) *BootImageID { + return getBootImageID(OptionSelectedBootImageID, o) +} diff --git a/dhcpv4/bsdp/bsdp.go b/dhcpv4/bsdp/bsdp.go index 3cc87d2..9bcc15d 100644 --- a/dhcpv4/bsdp/bsdp.go +++ b/dhcpv4/bsdp/bsdp.go @@ -30,20 +30,12 @@ type ReplyConfig struct { // ParseBootImageListFromAck parses the list of boot images presented in the // ACK[LIST] packet and returns them as a list of BootImages. -func ParseBootImageListFromAck(ack dhcpv4.DHCPv4) ([]BootImage, error) { - opt := ack.GetOneOption(dhcpv4.OptionVendorSpecificInformation) - if opt == nil { +func ParseBootImageListFromAck(ack *dhcpv4.DHCPv4) ([]BootImage, error) { + vendorOpts := GetVendorOptions(ack.Options) + if vendorOpts == nil { return nil, errors.New("ParseBootImageListFromAck: could not find vendor-specific option") } - vendorOpt, err := ParseOptVendorSpecificInformation(opt.ToBytes()) - if err != nil { - return nil, err - } - bootImageOpts := vendorOpt.GetOneOption(OptionBootImageList) - if bootImageOpts == nil { - return nil, fmt.Errorf("boot image option not found") - } - return bootImageOpts.(*OptBootImageList).Images, nil + return GetBootImageList(vendorOpts.Options), nil } func needsReplyPort(replyPort uint16) bool { @@ -53,28 +45,41 @@ func needsReplyPort(replyPort uint16) bool { // MessageTypeFromPacket extracts the BSDP message type (LIST, SELECT) from the // vendor-specific options and returns it. If the message type option cannot be // found, returns false. -func MessageTypeFromPacket(packet *dhcpv4.DHCPv4) *MessageType { - var ( - vendorOpts *OptVendorSpecificInformation - err error - ) - opt := packet.GetOneOption(dhcpv4.OptionVendorSpecificInformation) - if opt == nil { - return nil - } - if vendorOpts, err = ParseOptVendorSpecificInformation(opt.ToBytes()); err == nil { - if o := vendorOpts.GetOneOption(OptionMessageType); o != nil { - if optMessageType, ok := o.(*OptMessageType); ok { - return &optMessageType.Type - } - } - } - return nil +func MessageTypeFromPacket(packet *dhcpv4.DHCPv4) MessageType { + vendorOpts := GetVendorOptions(packet.Options) + if vendorOpts == nil { + return MessageTypeNone + } + return GetMessageType(vendorOpts.Options) +} + +// Packet is a BSDP packet wrapper around a DHCPv4 packet in order to print the +// correct vendor-specific BSDP information in String(). +type Packet struct { + dhcpv4.DHCPv4 +} + +// PacketFor returns a wrapped BSDP Packet given a DHCPv4 packet. +func PacketFor(d *dhcpv4.DHCPv4) *Packet { + return &Packet{*d} +} + +func (p Packet) v4() *dhcpv4.DHCPv4 { + return &p.DHCPv4 +} + +func (p Packet) String() string { + return p.DHCPv4.String() +} + +// Summary prints the BSDP packet with the correct vendor-specific options. +func (p Packet) Summary() string { + return p.DHCPv4.SummaryWithVendor(&VendorOptions{}) } // NewInformListForInterface creates a new INFORM packet for interface ifname // with configuration options specified by config. -func NewInformListForInterface(ifname string, replyPort uint16) (*dhcpv4.DHCPv4, error) { +func NewInformListForInterface(ifname string, replyPort uint16) (*Packet, error) { iface, err := net.InterfaceByName(ifname) if err != nil { return nil, err @@ -96,7 +101,7 @@ func NewInformListForInterface(ifname string, replyPort uint16) (*dhcpv4.DHCPv4, // NewInformList creates a new INFORM packet for interface with hardware address // `hwaddr` and IP `localIP`. Packet will be sent out on port `replyPort`. -func NewInformList(hwaddr net.HardwareAddr, localIP net.IP, replyPort uint16, modifiers ...dhcpv4.Modifier) (*dhcpv4.DHCPv4, error) { +func NewInformList(hwaddr net.HardwareAddr, localIP net.IP, replyPort uint16, modifiers ...dhcpv4.Modifier) (*Packet, error) { // Validate replyPort first if needsReplyPort(replyPort) && replyPort >= 1024 { return nil, errors.New("replyPort must be a privileged port") @@ -109,60 +114,61 @@ func NewInformList(hwaddr net.HardwareAddr, localIP net.IP, replyPort uint16, mo // These are vendor-specific options used to pass along BSDP information. vendorOpts := []dhcpv4.Option{ - &OptMessageType{MessageTypeList}, - Version1_1, + OptMessageType(MessageTypeList), + OptVersion(Version1_1), } if needsReplyPort(replyPort) { - vendorOpts = append(vendorOpts, &OptReplyPort{replyPort}) + vendorOpts = append(vendorOpts, OptReplyPort(replyPort)) } - return dhcpv4.NewInform(hwaddr, localIP, + d, err := dhcpv4.NewInform(hwaddr, localIP, dhcpv4.PrependModifiers(modifiers, dhcpv4.WithRequestedOptions( dhcpv4.OptionVendorSpecificInformation, dhcpv4.OptionClassIdentifier, ), - dhcpv4.WithOption(&dhcpv4.OptMaximumDHCPMessageSize{Size: MaxDHCPMessageSize}), - dhcpv4.WithOption(&dhcpv4.OptClassIdentifier{Identifier: vendorClassID}), - dhcpv4.WithOption(&OptVendorSpecificInformation{vendorOpts}), + dhcpv4.WithOption(dhcpv4.OptMaxMessageSize(MaxDHCPMessageSize)), + dhcpv4.WithOption(dhcpv4.OptClassIdentifier(vendorClassID)), + dhcpv4.WithOption(OptVendorOptions(vendorOpts...)), )...) + if err != nil { + return nil, err + } + return PacketFor(d), nil } // InformSelectForAck constructs an INFORM[SELECT] packet given an ACK to the // previously-sent INFORM[LIST]. -func InformSelectForAck(ack dhcpv4.DHCPv4, replyPort uint16, selectedImage BootImage) (*dhcpv4.DHCPv4, error) { +func InformSelectForAck(ack *Packet, replyPort uint16, selectedImage BootImage) (*Packet, error) { if needsReplyPort(replyPort) && replyPort >= 1024 { return nil, errors.New("replyPort must be a privileged port") } // Data for OptionSelectedBootImageID vendorOpts := []dhcpv4.Option{ - &OptMessageType{MessageTypeSelect}, - Version1_1, - &OptSelectedBootImageID{selectedImage.ID}, + OptMessageType(MessageTypeSelect), + OptVersion(Version1_1), + OptSelectedBootImageID(selectedImage.ID), } // Validate replyPort if requested. if needsReplyPort(replyPort) { - vendorOpts = append(vendorOpts, &OptReplyPort{replyPort}) + vendorOpts = append(vendorOpts, OptReplyPort(replyPort)) } // Find server IP address - var serverIP net.IP - if opt := ack.GetOneOption(dhcpv4.OptionServerIdentifier); opt != nil { - serverIP = opt.(*dhcpv4.OptServerIdentifier).ServerID - } + serverIP := dhcpv4.GetServerIdentifier(ack.Options) if serverIP.To4() == nil { return nil, fmt.Errorf("could not parse server identifier from ACK") } - vendorOpts = append(vendorOpts, &OptServerIdentifier{serverIP}) + vendorOpts = append(vendorOpts, OptServerIdentifier(serverIP)) vendorClassID, err := MakeVendorClassIdentifier() if err != nil { return nil, err } - return dhcpv4.New(dhcpv4.WithReply(&ack), - dhcpv4.WithOption(&dhcpv4.OptClassIdentifier{Identifier: vendorClassID}), + d, err := dhcpv4.New(dhcpv4.WithReply(ack.v4()), + dhcpv4.WithOption(dhcpv4.OptClassIdentifier(vendorClassID)), dhcpv4.WithRequestedOptions( dhcpv4.OptionSubnetMask, dhcpv4.OptionRouter, @@ -171,20 +177,24 @@ func InformSelectForAck(ack dhcpv4.DHCPv4, replyPort uint16, selectedImage BootI dhcpv4.OptionClassIdentifier, ), dhcpv4.WithMessageType(dhcpv4.MessageTypeInform), - dhcpv4.WithOption(&OptVendorSpecificInformation{vendorOpts}), + dhcpv4.WithOption(OptVendorOptions(vendorOpts...)), ) + if err != nil { + return nil, err + } + return PacketFor(d), nil } // NewReplyForInformList constructs an ACK for the INFORM[LIST] packet `inform` // with additional options in `config`. -func NewReplyForInformList(inform *dhcpv4.DHCPv4, config ReplyConfig) (*dhcpv4.DHCPv4, error) { +func NewReplyForInformList(inform *Packet, config ReplyConfig) (*Packet, error) { if config.DefaultImage == nil { return nil, errors.New("NewReplyForInformList: no default boot image ID set") } if config.Images == nil || len(config.Images) == 0 { return nil, errors.New("NewReplyForInformList: no boot images provided") } - reply, err := dhcpv4.NewReplyFromRequest(inform) + reply, err := dhcpv4.NewReplyFromRequest(&inform.DHCPv4) if err != nil { return nil, err } @@ -193,34 +203,34 @@ func NewReplyForInformList(inform *dhcpv4.DHCPv4, config ReplyConfig) (*dhcpv4.D reply.ServerIPAddr = config.ServerIP reply.ServerHostName = config.ServerHostname - reply.UpdateOption(&dhcpv4.OptMessageType{MessageType: dhcpv4.MessageTypeAck}) - reply.UpdateOption(&dhcpv4.OptServerIdentifier{ServerID: config.ServerIP}) - reply.UpdateOption(&dhcpv4.OptClassIdentifier{Identifier: AppleVendorID}) + reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) + reply.UpdateOption(dhcpv4.OptServerIdentifier(config.ServerIP)) + reply.UpdateOption(dhcpv4.OptClassIdentifier(AppleVendorID)) // BSDP opts. vendorOpts := []dhcpv4.Option{ - &OptMessageType{Type: MessageTypeList}, - &OptServerPriority{Priority: config.ServerPriority}, - &OptDefaultBootImageID{ID: config.DefaultImage.ID}, - &OptBootImageList{Images: config.Images}, + OptMessageType(MessageTypeList), + OptServerPriority(config.ServerPriority), + OptDefaultBootImageID(config.DefaultImage.ID), + OptBootImageList(config.Images...), } if config.SelectedImage != nil { - vendorOpts = append(vendorOpts, &OptSelectedBootImageID{ID: config.SelectedImage.ID}) + vendorOpts = append(vendorOpts, OptSelectedBootImageID(config.SelectedImage.ID)) } - reply.UpdateOption(&OptVendorSpecificInformation{Options: vendorOpts}) - return reply, nil + reply.UpdateOption(OptVendorOptions(vendorOpts...)) + return PacketFor(reply), nil } // NewReplyForInformSelect constructs an ACK for the INFORM[Select] packet // `inform` with additional options in `config`. -func NewReplyForInformSelect(inform *dhcpv4.DHCPv4, config ReplyConfig) (*dhcpv4.DHCPv4, error) { +func NewReplyForInformSelect(inform *Packet, config ReplyConfig) (*Packet, error) { if config.SelectedImage == nil { return nil, errors.New("NewReplyForInformSelect: no selected boot image ID set") } if config.Images == nil || len(config.Images) == 0 { return nil, errors.New("NewReplyForInformSelect: no boot images provided") } - reply, err := dhcpv4.NewReplyFromRequest(inform) + reply, err := dhcpv4.NewReplyFromRequest(&inform.DHCPv4) if err != nil { return nil, err } @@ -231,16 +241,14 @@ func NewReplyForInformSelect(inform *dhcpv4.DHCPv4, config ReplyConfig) (*dhcpv4 reply.ServerHostName = config.ServerHostname reply.BootFileName = config.BootFileName - reply.UpdateOption(&dhcpv4.OptMessageType{MessageType: dhcpv4.MessageTypeAck}) - reply.UpdateOption(&dhcpv4.OptServerIdentifier{ServerID: config.ServerIP}) - reply.UpdateOption(&dhcpv4.OptClassIdentifier{Identifier: AppleVendorID}) + reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) + reply.UpdateOption(dhcpv4.OptServerIdentifier(config.ServerIP)) + reply.UpdateOption(dhcpv4.OptClassIdentifier(AppleVendorID)) // BSDP opts. - reply.UpdateOption(&OptVendorSpecificInformation{ - Options: []dhcpv4.Option{ - &OptMessageType{Type: MessageTypeSelect}, - &OptSelectedBootImageID{ID: config.SelectedImage.ID}, - }, - }) - return reply, nil + reply.UpdateOption(OptVendorOptions( + OptMessageType(MessageTypeSelect), + OptSelectedBootImageID(config.SelectedImage.ID), + )) + return PacketFor(reply), nil } diff --git a/dhcpv4/bsdp/bsdp_option_boot_image_list.go b/dhcpv4/bsdp/bsdp_option_boot_image_list.go index 3282fa3..ebbbd2d 100644 --- a/dhcpv4/bsdp/bsdp_option_boot_image_list.go +++ b/dhcpv4/bsdp/bsdp_option_boot_image_list.go @@ -1,52 +1,66 @@ package bsdp import ( + "strings" + "github.com/insomniacslk/dhcp/dhcpv4" "github.com/u-root/u-root/pkg/uio" ) -// OptBootImageList contains the list of boot images presented by a netboot -// server. -type OptBootImageList struct { - Images []BootImage -} +// BootImageList contains a list of boot images presented by a netboot server. +// +// Implements the BSDP option listing the boot images. +type BootImageList []BootImage -// ParseOptBootImageList constructs an OptBootImageList struct from a sequence -// of bytes and returns it, or an error. -func ParseOptBootImageList(data []byte) (*OptBootImageList, error) { +// FromBytes deserializes data into bil. +func (bil *BootImageList) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - var bootImages []BootImage for buf.Has(5) { var image BootImage - if err := (&image).Unmarshal(buf); err != nil { - return nil, err + if err := image.Unmarshal(buf); err != nil { + return err } - bootImages = append(bootImages, image) + *bil = append(*bil, image) } - - return &OptBootImageList{bootImages}, nil -} - -// Code returns the option code. -func (o *OptBootImageList) Code() dhcpv4.OptionCode { - return OptionBootImageList + return nil } // ToBytes returns a serialized stream of bytes for this option. -func (o *OptBootImageList) ToBytes() []byte { +func (bil BootImageList) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) - for _, image := range o.Images { + for _, image := range bil { image.Marshal(buf) } return buf.Data() } // String returns a human-readable string for this option. -func (o *OptBootImageList) String() string { - s := "BSDP Boot Image List ->" - for _, image := range o.Images { - s += "\n " + image.String() +func (bil BootImageList) String() string { + s := make([]string, 0, len(bil)) + for _, image := range bil { + s = append(s, image.String()) + } + return strings.Join(s, ", ") +} + +// OptBootImageList returns a new BSDP boot image list. +func OptBootImageList(b ...BootImage) dhcpv4.Option { + return dhcpv4.Option{ + Code: OptionBootImageList, + Value: BootImageList(b), + } +} + +// GetBootImageList returns the BSDP boot image list. +func GetBootImageList(o dhcpv4.Options) BootImageList { + v := o.Get(OptionBootImageList) + if v == nil { + return nil + } + var bil BootImageList + if err := bil.FromBytes(v); err != nil { + return nil } - return s + return bil } diff --git a/dhcpv4/bsdp/bsdp_option_boot_image_list_test.go b/dhcpv4/bsdp/bsdp_option_boot_image_list_test.go index 5d1b77c..6282156 100644 --- a/dhcpv4/bsdp/bsdp_option_boot_image_list_test.go +++ b/dhcpv4/bsdp/bsdp_option_boot_image_list_test.go @@ -25,8 +25,8 @@ func TestOptBootImageListInterfaceMethods(t *testing.T) { Name: "bsdp-2", }, } - o := OptBootImageList{bs} - require.Equal(t, OptionBootImageList, o.Code(), "Code") + o := OptBootImageList(bs...) + require.Equal(t, OptionBootImageList, o.Code, "Code") expectedBytes := []byte{ // boot image 1 0x1, 0x0, 0x03, 0xe9, // ID @@ -37,7 +37,7 @@ func TestOptBootImageListInterfaceMethods(t *testing.T) { 6, // name length 'b', 's', 'd', 'p', '-', '2', } - require.Equal(t, expectedBytes, o.ToBytes(), "ToBytes") + require.Equal(t, expectedBytes, o.Value.ToBytes(), "ToBytes") } func TestParseOptBootImageList(t *testing.T) { @@ -51,9 +51,10 @@ func TestParseOptBootImageList(t *testing.T) { 6, // name length 'b', 's', 'd', 'p', '-', '2', } - o, err := ParseOptBootImageList(data) + var o BootImageList + err := o.FromBytes(data) require.NoError(t, err) - expectedBootImages := []BootImage{ + expectedBootImages := BootImageList{ BootImage{ ID: BootImageID{ IsInstall: false, @@ -71,7 +72,7 @@ func TestParseOptBootImageList(t *testing.T) { Name: "bsdp-2", }, } - require.Equal(t, &OptBootImageList{expectedBootImages}, o) + require.Equal(t, expectedBootImages, o) // Error parsing boot image (malformed) data = []byte{ @@ -84,7 +85,7 @@ func TestParseOptBootImageList(t *testing.T) { 6, // name length 'b', 's', 'd', 'p', '-', '2', } - _, err = ParseOptBootImageList(data) + err = o.FromBytes(data) require.Error(t, err, "should get error from bad boot image") } @@ -107,7 +108,7 @@ func TestOptBootImageListString(t *testing.T) { Name: "bsdp-2", }, } - o := OptBootImageList{bs} - expectedString := "BSDP Boot Image List ->\n bsdp-1 [1001] uninstallable macOS image\n bsdp-2 [9009] installable macOS 9 image" + o := OptBootImageList(bs...) + expectedString := "BSDP Boot Image List: bsdp-1 [1001] uninstallable macOS image, bsdp-2 [9009] installable macOS 9 image" require.Equal(t, expectedString, o.String()) } diff --git a/dhcpv4/bsdp/bsdp_option_default_boot_image_id.go b/dhcpv4/bsdp/bsdp_option_default_boot_image_id.go deleted file mode 100644 index 40ab0be..0000000 --- a/dhcpv4/bsdp/bsdp_option_default_boot_image_id.go +++ /dev/null @@ -1,42 +0,0 @@ -package bsdp - -import ( - "fmt" - - "github.com/insomniacslk/dhcp/dhcpv4" - "github.com/u-root/u-root/pkg/uio" -) - -// OptDefaultBootImageID contains the selected boot image ID. -// -// Implements the BSDP option default boot image ID, which tells the client -// which image is the default boot image if one is not selected. -type OptDefaultBootImageID struct { - ID BootImageID -} - -// ParseOptDefaultBootImageID constructs an OptDefaultBootImageID struct from a sequence of -// bytes and returns it, or an error. -func ParseOptDefaultBootImageID(data []byte) (*OptDefaultBootImageID, error) { - var o OptDefaultBootImageID - buf := uio.NewBigEndianBuffer(data) - if err := o.ID.Unmarshal(buf); err != nil { - return nil, err - } - return &o, buf.FinError() -} - -// Code returns the option code. -func (o *OptDefaultBootImageID) Code() dhcpv4.OptionCode { - return OptionDefaultBootImageID -} - -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptDefaultBootImageID) ToBytes() []byte { - return uio.ToBigEndian(o.ID) -} - -// String returns a human-readable string for this option. -func (o *OptDefaultBootImageID) String() string { - return fmt.Sprintf("BSDP Default Boot Image ID -> %s", o.ID.String()) -} diff --git a/dhcpv4/bsdp/bsdp_option_default_boot_image_id_test.go b/dhcpv4/bsdp/bsdp_option_default_boot_image_id_test.go deleted file mode 100644 index a5abdaf..0000000 --- a/dhcpv4/bsdp/bsdp_option_default_boot_image_id_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package bsdp - -import ( - "testing" - - "github.com/stretchr/testify/require" - "github.com/u-root/u-root/pkg/uio" -) - -func TestOptDefaultBootImageIDInterfaceMethods(t *testing.T) { - b := BootImageID{IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 1001} - o := OptDefaultBootImageID{b} - require.Equal(t, OptionDefaultBootImageID, o.Code(), "Code") - require.Equal(t, uio.ToBigEndian(b), o.ToBytes(), "ToBytes") -} - -func TestParseOptDefaultBootImageID(t *testing.T) { - b := BootImageID{IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 1001} - o, err := ParseOptDefaultBootImageID(uio.ToBigEndian(b)) - require.NoError(t, err) - require.Equal(t, &OptDefaultBootImageID{b}, o) - - // Short byte stream - data := []byte{} - _, err = ParseOptDefaultBootImageID(data) - require.Error(t, err, "should get error from short byte stream") - - // Bad length - data = []byte{1, 0, 0, 0, 0} - _, err = ParseOptDefaultBootImageID(data) - require.Error(t, err, "should get error from bad length") -} - -func TestOptDefaultBootImageIDString(t *testing.T) { - b := BootImageID{IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 1001} - o := OptDefaultBootImageID{b} - require.Equal(t, "BSDP Default Boot Image ID -> [1001] installable macOS image", o.String()) - - b = BootImageID{IsInstall: false, ImageType: BootImageTypeMacOS9, Index: 1001} - o = OptDefaultBootImageID{b} - require.Equal(t, "BSDP Default Boot Image ID -> [1001] uninstallable macOS 9 image", o.String()) - - b = BootImageID{IsInstall: false, ImageType: BootImageType(99), Index: 1001} - o = OptDefaultBootImageID{b} - require.Equal(t, "BSDP Default Boot Image ID -> [1001] uninstallable unknown image", o.String()) -} diff --git a/dhcpv4/bsdp/bsdp_option_generic.go b/dhcpv4/bsdp/bsdp_option_generic.go deleted file mode 100644 index e9e163f..0000000 --- a/dhcpv4/bsdp/bsdp_option_generic.go +++ /dev/null @@ -1,36 +0,0 @@ -package bsdp - -import ( - "fmt" - - "github.com/insomniacslk/dhcp/dhcpv4" -) - -// OptGeneric is an option that only contains the option code and associated -// data. Every option that does not have a specific implementation will fall -// back to this option. -type OptGeneric struct { - OptionCode dhcpv4.OptionCode - Data []byte -} - -// ParseOptGeneric parses a bytestream and creates a new OptGeneric from it, -// or an error. -func ParseOptGeneric(code dhcpv4.OptionCode, data []byte) (*OptGeneric, error) { - return &OptGeneric{OptionCode: code, Data: data}, nil -} - -// Code returns the generic option code. -func (o OptGeneric) Code() dhcpv4.OptionCode { - return o.OptionCode -} - -// ToBytes returns a serialized generic option as a slice of bytes. -func (o OptGeneric) ToBytes() []byte { - return o.Data -} - -// String returns a human-readable representation of a generic option. -func (o OptGeneric) String() string { - return fmt.Sprintf("%s -> %v", o.OptionCode, o.Data) -} diff --git a/dhcpv4/bsdp/bsdp_option_generic_test.go b/dhcpv4/bsdp/bsdp_option_generic_test.go deleted file mode 100644 index a813f95..0000000 --- a/dhcpv4/bsdp/bsdp_option_generic_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package bsdp - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestParseOptGeneric(t *testing.T) { - // Good parse - o, err := ParseOptGeneric(OptionMessageType, []byte{1}) - require.NoError(t, err) - require.Equal(t, OptionMessageType, o.Code()) - require.Equal(t, MessageTypeList, MessageType(o.Data[0])) -} - -func TestOptGenericCode(t *testing.T) { - o := OptGeneric{ - OptionCode: OptionMessageType, - Data: []byte{byte(MessageTypeList)}, - } - require.Equal(t, OptionMessageType, o.Code()) -} - -func TestOptGenericData(t *testing.T) { - o := OptGeneric{ - OptionCode: OptionServerIdentifier, - Data: []byte{192, 168, 0, 1}, - } - require.Equal(t, []byte{192, 168, 0, 1}, o.Data) -} - -func TestOptGenericToBytes(t *testing.T) { - o := OptGeneric{ - OptionCode: OptionServerIdentifier, - Data: []byte{192, 168, 0, 1}, - } - serialized := o.ToBytes() - expected := []byte{192, 168, 0, 1} - require.Equal(t, expected, serialized) -} - -func TestOptGenericString(t *testing.T) { - o := OptGeneric{ - OptionCode: OptionServerIdentifier, - Data: []byte{192, 168, 0, 1}, - } - require.Equal(t, "BSDP Server Identifier -> [192 168 0 1]", o.String()) -} - -func TestOptGenericStringUnknown(t *testing.T) { - o := OptGeneric{ - OptionCode: optionCode(102), // Returned option code. - Data: []byte{5}, - } - require.Equal(t, "unknown -> [5]", o.String()) -} diff --git a/dhcpv4/bsdp/bsdp_option_machine_name.go b/dhcpv4/bsdp/bsdp_option_machine_name.go deleted file mode 100644 index ced88b0..0000000 --- a/dhcpv4/bsdp/bsdp_option_machine_name.go +++ /dev/null @@ -1,34 +0,0 @@ -package bsdp - -import ( - "github.com/insomniacslk/dhcp/dhcpv4" -) - -// OptMachineName represents a BSDP message type. -// -// Implements the BSDP option machine name, which gives the Netboot server's -// machine name. -type OptMachineName struct { - Name string -} - -// ParseOptMachineName constructs an OptMachineName struct from a sequence of -// bytes and returns it, or an error. -func ParseOptMachineName(data []byte) (*OptMachineName, error) { - return &OptMachineName{Name: string(data)}, nil -} - -// Code returns the option code. -func (o *OptMachineName) Code() dhcpv4.OptionCode { - return OptionMachineName -} - -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptMachineName) ToBytes() []byte { - return []byte(o.Name) -} - -// String returns a human-readable string for this option. -func (o *OptMachineName) String() string { - return "BSDP Machine Name -> " + o.Name -} diff --git a/dhcpv4/bsdp/bsdp_option_machine_name_test.go b/dhcpv4/bsdp/bsdp_option_machine_name_test.go deleted file mode 100644 index abc0d54..0000000 --- a/dhcpv4/bsdp/bsdp_option_machine_name_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package bsdp - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestOptMachineNameInterfaceMethods(t *testing.T) { - o := OptMachineName{"somebox"} - require.Equal(t, OptionMachineName, o.Code(), "Code") - expectedBytes := []byte{'s', 'o', 'm', 'e', 'b', 'o', 'x'} - require.Equal(t, expectedBytes, o.ToBytes(), "ToBytes") -} - -func TestParseOptMachineName(t *testing.T) { - data := []byte{'s', 'o', 'm', 'e', 'b', 'o', 'x'} - o, err := ParseOptMachineName(data) - require.NoError(t, err) - require.Equal(t, &OptMachineName{"somebox"}, o) -} - -func TestOptMachineNameString(t *testing.T) { - o := OptMachineName{"somebox"} - require.Equal(t, "BSDP Machine Name -> somebox", o.String()) -} diff --git a/dhcpv4/bsdp/bsdp_option_message_type.go b/dhcpv4/bsdp/bsdp_option_message_type.go index 5f96f12..cb0c5cf 100644 --- a/dhcpv4/bsdp/bsdp_option_message_type.go +++ b/dhcpv4/bsdp/bsdp_option_message_type.go @@ -15,16 +15,23 @@ type MessageType byte // BSDP Message types - e.g. LIST, SELECT, FAILED const ( + MessageTypeNone MessageType = 0 MessageTypeList MessageType = 1 MessageTypeSelect MessageType = 2 MessageTypeFailed MessageType = 3 ) +// ToBytes returns a serialized stream of bytes for this option. +func (m MessageType) ToBytes() []byte { + return []byte{byte(m)} +} + +// String returns a human-friendly representation of MessageType. func (m MessageType) String() string { if s, ok := messageTypeToString[m]; ok { return s } - return "Unknown" + return fmt.Sprintf("unknown (%d)", m) } // messageTypeToString maps each BSDP message type to a human-readable string. @@ -34,29 +41,30 @@ var messageTypeToString = map[MessageType]string{ MessageTypeFailed: "FAILED", } -// OptMessageType represents a BSDP message type. -type OptMessageType struct { - Type MessageType -} - -// ParseOptMessageType constructs an OptMessageType struct from a sequence of -// bytes and returns it, or an error. -func ParseOptMessageType(data []byte) (*OptMessageType, error) { +// FromBytes reads data into m. +func (m *MessageType) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - return &OptMessageType{Type: MessageType(buf.Read8())}, buf.FinError() + *m = MessageType(buf.Read8()) + return buf.FinError() } -// Code returns the option code. -func (o *OptMessageType) Code() dhcpv4.OptionCode { - return OptionMessageType -} - -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptMessageType) ToBytes() []byte { - return []byte{byte(o.Type)} +// OptMessageType returns a new BSDP Message Type option. +func OptMessageType(mt MessageType) dhcpv4.Option { + return dhcpv4.Option{ + Code: OptionMessageType, + Value: mt, + } } -// String returns a human-readable string for this option. -func (o *OptMessageType) String() string { - return fmt.Sprintf("BSDP Message Type -> %s", o.Type.String()) +// GetMessageType returns the BSDP Message Type in o. +func GetMessageType(o dhcpv4.Options) MessageType { + v := o.Get(OptionMessageType) + if v == nil { + return MessageTypeNone + } + var m MessageType + if err := m.FromBytes(v); err != nil { + return MessageTypeNone + } + return m } diff --git a/dhcpv4/bsdp/bsdp_option_message_type_test.go b/dhcpv4/bsdp/bsdp_option_message_type_test.go index a6695cc..6666137 100644 --- a/dhcpv4/bsdp/bsdp_option_message_type_test.go +++ b/dhcpv4/bsdp/bsdp_option_message_type_test.go @@ -7,24 +7,25 @@ import ( ) func TestOptMessageTypeInterfaceMethods(t *testing.T) { - o := OptMessageType{MessageTypeList} - require.Equal(t, OptionMessageType, o.Code(), "Code") - require.Equal(t, []byte{1}, o.ToBytes(), "ToBytes") + o := OptMessageType(MessageTypeList) + require.Equal(t, OptionMessageType, o.Code, "Code") + require.Equal(t, []byte{1}, o.Value.ToBytes(), "ToBytes") } func TestParseOptMessageType(t *testing.T) { + var o MessageType data := []byte{1} // DISCOVER - o, err := ParseOptMessageType(data) + err := o.FromBytes(data) require.NoError(t, err) - require.Equal(t, &OptMessageType{MessageTypeList}, o) + require.Equal(t, MessageTypeList, o) } func TestOptMessageTypeString(t *testing.T) { // known - o := OptMessageType{MessageTypeList} - require.Equal(t, "BSDP Message Type -> LIST", o.String()) + o := OptMessageType(MessageTypeList) + require.Equal(t, "BSDP Message Type: LIST", o.String()) // unknown - o = OptMessageType{99} - require.Equal(t, "BSDP Message Type -> Unknown", o.String()) + o = OptMessageType(99) + require.Equal(t, "BSDP Message Type: unknown (99)", o.String()) } diff --git a/dhcpv4/bsdp/bsdp_option_misc.go b/dhcpv4/bsdp/bsdp_option_misc.go new file mode 100644 index 0000000..2d3a7bf --- /dev/null +++ b/dhcpv4/bsdp/bsdp_option_misc.go @@ -0,0 +1,99 @@ +package bsdp + +import ( + "fmt" + "net" + + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/u-root/u-root/pkg/uio" +) + +// OptReplyPort returns a new BSDP reply port option. +// +// Implements the BSDP option reply port. This is used when BSDP responses +// should be sent to a reply port other than the DHCP default. The macOS GUI +// "Startup Disk Select" sends this option since it's operating in an +// unprivileged context. +func OptReplyPort(port uint16) dhcpv4.Option { + return dhcpv4.Option{Code: OptionReplyPort, Value: dhcpv4.Uint16(port)} +} + +// GetReplyPort returns the BSDP reply port in o, if present. +func GetReplyPort(o dhcpv4.Options) (uint16, error) { + return dhcpv4.GetUint16(OptionReplyPort, o) +} + +// OptServerPriority returns a new BSDP server priority option. +func OptServerPriority(prio uint16) dhcpv4.Option { + return dhcpv4.Option{Code: OptionServerPriority, Value: dhcpv4.Uint16(prio)} +} + +// GetServerPriority returns the BSDP server priority in o if present. +func GetServerPriority(o dhcpv4.Options) (uint16, error) { + return dhcpv4.GetUint16(OptionServerPriority, o) +} + +// OptMachineName returns a BSDP Machine Name option. +func OptMachineName(name string) dhcpv4.Option { + return dhcpv4.Option{Code: OptionMachineName, Value: dhcpv4.String(name)} +} + +// GetMachineName finds and parses the BSDP Machine Name option from o. +func GetMachineName(o dhcpv4.Options) string { + return dhcpv4.GetString(OptionMachineName, o) +} + +// Version is the BSDP protocol version. Can be one of 1.0 or 1.1. +type Version [2]byte + +// Specific versions. +var ( + Version1_0 = Version{1, 0} + Version1_1 = Version{1, 1} +) + +// ToBytes returns a serialized stream of bytes for this option. +func (o Version) ToBytes() []byte { + return o[:] +} + +// String returns a human-readable string for this option. +func (o Version) String() string { + return fmt.Sprintf("%d.%d", o[0], o[1]) +} + +// FromBytes constructs a Version struct from a sequence of +// bytes and returns it, or an error. +func (o *Version) FromBytes(data []byte) error { + buf := uio.NewBigEndianBuffer(data) + buf.ReadBytes(o[:]) + return buf.FinError() +} + +// OptVersion returns a new BSDP version option. +func OptVersion(version Version) dhcpv4.Option { + return dhcpv4.Option{Code: OptionVersion, Value: version} +} + +// GetVersion returns the BSDP version in o if present. +func GetVersion(o dhcpv4.Options) (Version, error) { + v := o.Get(OptionVersion) + if v == nil { + return Version{0, 0}, fmt.Errorf("version not found") + } + var ver Version + if err := ver.FromBytes(v); err != nil { + return Version{0, 0}, err + } + return ver, nil +} + +// GetServerIdentifier returns the BSDP Server Identifier value in o. +func GetServerIdentifier(o dhcpv4.Options) net.IP { + return dhcpv4.GetIP(OptionServerIdentifier, o) +} + +// OptServerIdentifier returns a new BSDP Server Identifier option. +func OptServerIdentifier(ip net.IP) dhcpv4.Option { + return dhcpv4.Option{Code: OptionServerIdentifier, Value: dhcpv4.IP(ip)} +} diff --git a/dhcpv4/bsdp/bsdp_option_misc_test.go b/dhcpv4/bsdp/bsdp_option_misc_test.go new file mode 100644 index 0000000..dfa81b5 --- /dev/null +++ b/dhcpv4/bsdp/bsdp_option_misc_test.go @@ -0,0 +1,95 @@ +package bsdp + +import ( + "net" + "testing" + + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/stretchr/testify/require" +) + +func TestOptReplyPort(t *testing.T) { + o := OptReplyPort(1234) + require.Equal(t, OptionReplyPort, o.Code, "Code") + require.Equal(t, []byte{4, 210}, o.Value.ToBytes(), "ToBytes") + require.Equal(t, "BSDP Reply Port: 1234", o.String()) +} + +func TestGetReplyPort(t *testing.T) { + o := VendorOptions{dhcpv4.OptionsFromList(OptReplyPort(1234))} + port, err := GetReplyPort(o.Options) + require.NoError(t, err) + require.Equal(t, uint16(1234), port) + + port, err = GetReplyPort(dhcpv4.Options{}) + require.Error(t, err, "no reply port present") +} + +func TestOptServerPriority(t *testing.T) { + o := OptServerPriority(1234) + require.Equal(t, OptionServerPriority, o.Code, "Code") + require.Equal(t, []byte{4, 210}, o.Value.ToBytes(), "ToBytes") + require.Equal(t, "BSDP Server Priority: 1234", o.String()) +} + +func TestGetServerPriority(t *testing.T) { + o := VendorOptions{dhcpv4.OptionsFromList(OptServerPriority(1234))} + prio, err := GetServerPriority(o.Options) + require.NoError(t, err) + require.Equal(t, uint16(1234), prio) + + prio, err = GetServerPriority(dhcpv4.Options{}) + require.Error(t, err, "no server prio present") +} + +func TestOptMachineName(t *testing.T) { + o := OptMachineName("foo") + require.Equal(t, OptionMachineName, o.Code, "Code") + require.Equal(t, []byte("foo"), o.Value.ToBytes(), "ToBytes") + require.Equal(t, "BSDP Machine Name: foo", o.String()) +} + +func TestGetMachineName(t *testing.T) { + o := VendorOptions{dhcpv4.OptionsFromList(OptMachineName("foo"))} + require.Equal(t, "foo", GetMachineName(o.Options)) + require.Equal(t, "", GetMachineName(dhcpv4.Options{})) +} + +func TestOptVersion(t *testing.T) { + o := OptVersion(Version1_1) + require.Equal(t, OptionVersion, o.Code, "Code") + require.Equal(t, []byte{1, 1}, o.Value.ToBytes(), "ToBytes") + require.Equal(t, "BSDP Version: 1.1", o.String()) +} + +func TestGetVersion(t *testing.T) { + o := VendorOptions{dhcpv4.OptionsFromList(OptVersion(Version1_1))} + ver, err := GetVersion(o.Options) + require.NoError(t, err) + require.Equal(t, ver, Version1_1) + + ver, err = GetVersion(dhcpv4.Options{}) + require.Error(t, err, "no version present") + + ver, err = GetVersion(dhcpv4.Options{OptionVersion.Code(): []byte{}}) + require.Error(t, err, "empty version field") + + ver, err = GetVersion(dhcpv4.Options{OptionVersion.Code(): []byte{1}}) + require.Error(t, err, "version option too short") + + ver, err = GetVersion(dhcpv4.Options{OptionVersion.Code(): []byte{1, 2, 3}}) + require.Error(t, err, "version option too long") +} + +func TestOptServerIdentifier(t *testing.T) { + o := OptServerIdentifier(net.IP{1, 1, 1, 1}) + require.Equal(t, OptionServerIdentifier, o.Code, "Code") + require.Equal(t, []byte{1, 1, 1, 1}, o.Value.ToBytes(), "ToBytes") + require.Equal(t, "BSDP Server Identifier: 1.1.1.1", o.String()) +} + +func TestGetServerIdentifier(t *testing.T) { + o := VendorOptions{dhcpv4.OptionsFromList(OptServerIdentifier(net.IP{1, 1, 1, 1}))} + require.Equal(t, net.IP{1, 1, 1, 1}, GetServerIdentifier(o.Options)) + require.Equal(t, net.IP(nil), GetServerIdentifier(dhcpv4.Options{})) +} diff --git a/dhcpv4/bsdp/bsdp_option_reply_port.go b/dhcpv4/bsdp/bsdp_option_reply_port.go deleted file mode 100644 index 5eea5ee..0000000 --- a/dhcpv4/bsdp/bsdp_option_reply_port.go +++ /dev/null @@ -1,42 +0,0 @@ -package bsdp - -import ( - "fmt" - - "github.com/insomniacslk/dhcp/dhcpv4" - "github.com/u-root/u-root/pkg/uio" -) - -// OptReplyPort represents a BSDP protocol version. -// -// Implements the BSDP option reply port. This is used when BSDP responses -// should be sent to a reply port other than the DHCP default. The macOS GUI -// "Startup Disk Select" sends this option since it's operating in an -// unprivileged context. -type OptReplyPort struct { - Port uint16 -} - -// ParseOptReplyPort constructs an OptReplyPort struct from a sequence of -// bytes and returns it, or an error. -func ParseOptReplyPort(data []byte) (*OptReplyPort, error) { - buf := uio.NewBigEndianBuffer(data) - return &OptReplyPort{buf.Read16()}, buf.FinError() -} - -// Code returns the option code. -func (o *OptReplyPort) Code() dhcpv4.OptionCode { - return OptionReplyPort -} - -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptReplyPort) ToBytes() []byte { - buf := uio.NewBigEndianBuffer(nil) - buf.Write16(o.Port) - return buf.Data() -} - -// String returns a human-readable string for this option. -func (o *OptReplyPort) String() string { - return fmt.Sprintf("BSDP Reply Port -> %v", o.Port) -} diff --git a/dhcpv4/bsdp/bsdp_option_reply_port_test.go b/dhcpv4/bsdp/bsdp_option_reply_port_test.go deleted file mode 100644 index de94ffb..0000000 --- a/dhcpv4/bsdp/bsdp_option_reply_port_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package bsdp - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestOptReplyPortInterfaceMethods(t *testing.T) { - o := OptReplyPort{1234} - require.Equal(t, OptionReplyPort, o.Code(), "Code") - require.Equal(t, []byte{4, 210}, o.ToBytes(), "ToBytes") -} - -func TestParseOptReplyPort(t *testing.T) { - data := []byte{0, 1} - o, err := ParseOptReplyPort(data) - require.NoError(t, err) - require.Equal(t, &OptReplyPort{1}, o) - - // Short byte stream - data = []byte{} - _, err = ParseOptReplyPort(data) - require.Error(t, err, "should get error from short byte stream") - - // Bad length - data = []byte{1} - _, err = ParseOptReplyPort(data) - require.Error(t, err, "should get error from bad length") -} - -func TestOptReplyPortString(t *testing.T) { - // known - o := OptReplyPort{1234} - require.Equal(t, "BSDP Reply Port -> 1234", o.String()) -} diff --git a/dhcpv4/bsdp/bsdp_option_selected_boot_image_id.go b/dhcpv4/bsdp/bsdp_option_selected_boot_image_id.go deleted file mode 100644 index 67f99a8..0000000 --- a/dhcpv4/bsdp/bsdp_option_selected_boot_image_id.go +++ /dev/null @@ -1,42 +0,0 @@ -package bsdp - -import ( - "fmt" - - "github.com/insomniacslk/dhcp/dhcpv4" - "github.com/u-root/u-root/pkg/uio" -) - -// OptSelectedBootImageID contains the selected boot image ID. -// -// Implements the BSDP option selected boot image ID, which tells the server -// which boot image has been selected by the client. -type OptSelectedBootImageID struct { - ID BootImageID -} - -// ParseOptSelectedBootImageID constructs an OptSelectedBootImageID struct from a sequence of -// bytes and returns it, or an error. -func ParseOptSelectedBootImageID(data []byte) (*OptSelectedBootImageID, error) { - var o OptSelectedBootImageID - buf := uio.NewBigEndianBuffer(data) - if err := o.ID.Unmarshal(buf); err != nil { - return nil, err - } - return &o, buf.FinError() -} - -// Code returns the option code. -func (o *OptSelectedBootImageID) Code() dhcpv4.OptionCode { - return OptionSelectedBootImageID -} - -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptSelectedBootImageID) ToBytes() []byte { - return uio.ToBigEndian(o.ID) -} - -// String returns a human-readable string for this option. -func (o *OptSelectedBootImageID) String() string { - return fmt.Sprintf("BSDP Selected Boot Image ID -> %s", o.ID.String()) -} diff --git a/dhcpv4/bsdp/bsdp_option_selected_boot_image_id_test.go b/dhcpv4/bsdp/bsdp_option_selected_boot_image_id_test.go deleted file mode 100644 index e187fc7..0000000 --- a/dhcpv4/bsdp/bsdp_option_selected_boot_image_id_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package bsdp - -import ( - "testing" - - "github.com/stretchr/testify/require" - "github.com/u-root/u-root/pkg/uio" -) - -func TestOptSelectedBootImageIDInterfaceMethods(t *testing.T) { - b := BootImageID{IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 1001} - o := OptSelectedBootImageID{b} - require.Equal(t, OptionSelectedBootImageID, o.Code(), "Code") - require.Equal(t, uio.ToBigEndian(b), o.ToBytes(), "ToBytes") -} - -func TestParseOptSelectedBootImageID(t *testing.T) { - b := BootImageID{IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 1001} - o, err := ParseOptSelectedBootImageID(uio.ToBigEndian(b)) - require.NoError(t, err) - require.Equal(t, &OptSelectedBootImageID{b}, o) - - // Short byte stream - data := []byte{} - _, err = ParseOptSelectedBootImageID(data) - require.Error(t, err, "should get error from short byte stream") - - // Bad length - data = []byte{1, 0, 0, 0, 0} - _, err = ParseOptSelectedBootImageID(data) - require.Error(t, err, "should get error from bad length") -} - -func TestOptSelectedBootImageIDString(t *testing.T) { - b := BootImageID{IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 1001} - o := OptSelectedBootImageID{b} - require.Equal(t, "BSDP Selected Boot Image ID -> [1001] installable macOS image", o.String()) - - b = BootImageID{IsInstall: false, ImageType: BootImageTypeMacOS9, Index: 1001} - o = OptSelectedBootImageID{b} - require.Equal(t, "BSDP Selected Boot Image ID -> [1001] uninstallable macOS 9 image", o.String()) - - b = BootImageID{IsInstall: false, ImageType: BootImageType(99), Index: 1001} - o = OptSelectedBootImageID{b} - require.Equal(t, "BSDP Selected Boot Image ID -> [1001] uninstallable unknown image", o.String()) -} diff --git a/dhcpv4/bsdp/bsdp_option_server_identifier.go b/dhcpv4/bsdp/bsdp_option_server_identifier.go deleted file mode 100644 index d1f5b6c..0000000 --- a/dhcpv4/bsdp/bsdp_option_server_identifier.go +++ /dev/null @@ -1,36 +0,0 @@ -package bsdp - -import ( - "fmt" - "net" - - "github.com/insomniacslk/dhcp/dhcpv4" - "github.com/u-root/u-root/pkg/uio" -) - -// OptServerIdentifier implements the BSDP server identifier option. -type OptServerIdentifier struct { - ServerID net.IP -} - -// ParseOptServerIdentifier returns a new OptServerIdentifier from a byte -// stream, or error if any. -func ParseOptServerIdentifier(data []byte) (*OptServerIdentifier, error) { - buf := uio.NewBigEndianBuffer(data) - return &OptServerIdentifier{ServerID: net.IP(buf.CopyN(net.IPv4len))}, buf.FinError() -} - -// Code returns the option code. -func (o *OptServerIdentifier) Code() dhcpv4.OptionCode { - return OptionServerIdentifier -} - -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptServerIdentifier) ToBytes() []byte { - return o.ServerID.To4() -} - -// String returns a human-readable string. -func (o *OptServerIdentifier) String() string { - return fmt.Sprintf("BSDP Server Identifier -> %v", o.ServerID.String()) -} diff --git a/dhcpv4/bsdp/bsdp_option_server_identifier_test.go b/dhcpv4/bsdp/bsdp_option_server_identifier_test.go deleted file mode 100644 index 5a77644..0000000 --- a/dhcpv4/bsdp/bsdp_option_server_identifier_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package bsdp - -import ( - "net" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestOptServerIdentifierInterfaceMethods(t *testing.T) { - ip := net.IP{192, 168, 0, 1} - o := OptServerIdentifier{ServerID: ip} - require.Equal(t, OptionServerIdentifier, o.Code(), "Code") - expectedBytes := []byte{192, 168, 0, 1} - require.Equal(t, expectedBytes, o.ToBytes(), "ToBytes") - require.Equal(t, "BSDP Server Identifier -> 192.168.0.1", o.String(), "String") -} - -func TestParseOptServerIdentifier(t *testing.T) { - var ( - o *OptServerIdentifier - err error - ) - o, err = ParseOptServerIdentifier([]byte{}) - require.Error(t, err, "empty byte stream") - - o, err = ParseOptServerIdentifier([]byte{3, 4, 192}) - require.Error(t, err, "wrong IP length") - - o, err = ParseOptServerIdentifier([]byte{192, 168, 0, 1}) - require.NoError(t, err) - require.Equal(t, net.IP{192, 168, 0, 1}, o.ServerID) -} diff --git a/dhcpv4/bsdp/bsdp_option_server_priority.go b/dhcpv4/bsdp/bsdp_option_server_priority.go deleted file mode 100644 index f6fcf57..0000000 --- a/dhcpv4/bsdp/bsdp_option_server_priority.go +++ /dev/null @@ -1,37 +0,0 @@ -package bsdp - -import ( - "fmt" - - "github.com/insomniacslk/dhcp/dhcpv4" - "github.com/u-root/u-root/pkg/uio" -) - -// OptServerPriority represents an option encapsulating the server priority. -type OptServerPriority struct { - Priority uint16 -} - -// ParseOptServerPriority returns a new OptServerPriority from a byte stream, or -// error if any. -func ParseOptServerPriority(data []byte) (*OptServerPriority, error) { - buf := uio.NewBigEndianBuffer(data) - return &OptServerPriority{Priority: buf.Read16()}, buf.FinError() -} - -// Code returns the option code. -func (o *OptServerPriority) Code() dhcpv4.OptionCode { - return OptionServerPriority -} - -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptServerPriority) ToBytes() []byte { - buf := uio.NewBigEndianBuffer(nil) - buf.Write16(o.Priority) - return buf.Data() -} - -// String returns a human-readable string. -func (o *OptServerPriority) String() string { - return fmt.Sprintf("BSDP Server Priority -> %v", o.Priority) -} diff --git a/dhcpv4/bsdp/bsdp_option_server_priority_test.go b/dhcpv4/bsdp/bsdp_option_server_priority_test.go deleted file mode 100644 index c4c96de..0000000 --- a/dhcpv4/bsdp/bsdp_option_server_priority_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package bsdp - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestOptServerPriorityInterfaceMethods(t *testing.T) { - o := OptServerPriority{Priority: 100} - require.Equal(t, OptionServerPriority, o.Code(), "Code") - require.Equal(t, []byte{0, 100}, o.ToBytes(), "ToBytes") - require.Equal(t, "BSDP Server Priority -> 100", o.String(), "String") -} - -func TestParseOptServerPriority(t *testing.T) { - var ( - o *OptServerPriority - err error - ) - o, err = ParseOptServerPriority([]byte{}) - require.Error(t, err, "empty byte stream") - - o, err = ParseOptServerPriority([]byte{1}) - require.Error(t, err, "short byte stream") - - o, err = ParseOptServerPriority([]byte{0, 100}) - require.NoError(t, err) - require.Equal(t, uint16(100), o.Priority) -} diff --git a/dhcpv4/bsdp/bsdp_option_version.go b/dhcpv4/bsdp/bsdp_option_version.go deleted file mode 100644 index d6b78c8..0000000 --- a/dhcpv4/bsdp/bsdp_option_version.go +++ /dev/null @@ -1,41 +0,0 @@ -package bsdp - -import ( - "fmt" - - "github.com/insomniacslk/dhcp/dhcpv4" - "github.com/u-root/u-root/pkg/uio" -) - -// Version is the BSDP protocol version. Can be one of 1.0 or 1.1. -type Version [2]byte - -// Specific versions. -var ( - Version1_0 = Version{1, 0} - Version1_1 = Version{1, 1} -) - -// ParseOptVersion constructs an OptVersion struct from a sequence of -// bytes and returns it, or an error. -func ParseOptVersion(data []byte) (Version, error) { - buf := uio.NewBigEndianBuffer(data) - var v Version - buf.ReadBytes(v[:]) - return v, buf.FinError() -} - -// Code returns the option code. -func (o Version) Code() dhcpv4.OptionCode { - return OptionVersion -} - -// ToBytes returns a serialized stream of bytes for this option. -func (o Version) ToBytes() []byte { - return o[:] -} - -// String returns a human-readable string for this option. -func (o Version) String() string { - return fmt.Sprintf("BSDP Version -> %d.%d", o[0], o[1]) -} diff --git a/dhcpv4/bsdp/bsdp_option_version_test.go b/dhcpv4/bsdp/bsdp_option_version_test.go deleted file mode 100644 index 69d4c86..0000000 --- a/dhcpv4/bsdp/bsdp_option_version_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package bsdp - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestOptVersionInterfaceMethods(t *testing.T) { - o := Version1_1 - require.Equal(t, OptionVersion, o.Code(), "Code") - require.Equal(t, []byte{1, 1}, o.ToBytes(), "ToBytes") -} - -func TestParseOptVersion(t *testing.T) { - data := []byte{1, 1} - o, err := ParseOptVersion(data) - require.NoError(t, err) - require.Equal(t, Version1_1, o) - - // Short byte stream - data = []byte{2} - _, err = ParseOptVersion(data) - require.Error(t, err, "should get error from short byte stream") -} - -func TestOptVersionString(t *testing.T) { - require.Equal(t, "BSDP Version -> 1.1", Version1_1.String()) -} diff --git a/dhcpv4/bsdp/bsdp_test.go b/dhcpv4/bsdp/bsdp_test.go index 638a408..e0378c2 100644 --- a/dhcpv4/bsdp/bsdp_test.go +++ b/dhcpv4/bsdp/bsdp_test.go @@ -12,9 +12,9 @@ import ( func RequireHasOption(t *testing.T, opts dhcpv4.Options, opt dhcpv4.Option) { require.NotNil(t, opts, "must pass list of options") require.NotNil(t, opt, "must pass option") - require.True(t, opts.Has(opt.Code())) - actual := opts.GetOne(opt.Code()) - require.Equal(t, opt, actual) + require.True(t, opts.Has(opt.Code)) + actual := opts.Get(opt.Code) + require.Equal(t, opt.Value.ToBytes(), actual) } func TestParseBootImageListFromAck(t *testing.T) { @@ -37,11 +37,11 @@ func TestParseBootImageListFromAck(t *testing.T) { }, } ack, _ := dhcpv4.New() - ack.UpdateOption(&OptVendorSpecificInformation{ - []dhcpv4.Option{&OptBootImageList{expectedBootImages}}, - }) + ack.UpdateOption(OptVendorOptions( + OptBootImageList(expectedBootImages...), + )) - images, err := ParseBootImageListFromAck(*ack) + images, err := ParseBootImageListFromAck(ack) require.NoError(t, err) require.NotEmpty(t, images, "should get BootImages") require.Equal(t, expectedBootImages, images, "should get same BootImages") @@ -49,7 +49,7 @@ func TestParseBootImageListFromAck(t *testing.T) { func TestParseBootImageListFromAckNoVendorOption(t *testing.T) { ack, _ := dhcpv4.New() - images, err := ParseBootImageListFromAck(*ack) + images, err := ParseBootImageListFromAck(ack) require.Error(t, err) require.Empty(t, images, "no BootImages") } @@ -70,14 +70,13 @@ func TestNewInformList_NoReplyPort(t *testing.T) { require.True(t, m.Options.Has(dhcpv4.OptionParameterRequestList)) require.True(t, m.Options.Has(dhcpv4.OptionMaximumDHCPMessageSize)) - opt := m.GetOneOption(dhcpv4.OptionVendorSpecificInformation) - require.NotNil(t, opt, "vendor opts not present") - vendorInfo := opt.(*OptVendorSpecificInformation) - require.True(t, vendorInfo.Options.Has(OptionMessageType)) - require.True(t, vendorInfo.Options.Has(OptionVersion)) + vendorOpts := GetVendorOptions(m.Options) + require.NotNil(t, vendorOpts, "vendor opts not present") + require.True(t, vendorOpts.Has(OptionMessageType)) + require.True(t, vendorOpts.Has(OptionVersion)) - opt = vendorInfo.GetOneOption(OptionMessageType) - require.Equal(t, MessageTypeList, opt.(*OptMessageType).Type) + mt := GetMessageType(vendorOpts.Options) + require.Equal(t, MessageTypeList, mt) } func TestNewInformList_ReplyPort(t *testing.T) { @@ -94,12 +93,12 @@ func TestNewInformList_ReplyPort(t *testing.T) { m, err := NewInformList(hwAddr, localIP, replyPort) require.NoError(t, err) - opt := m.GetOneOption(dhcpv4.OptionVendorSpecificInformation) - vendorInfo := opt.(*OptVendorSpecificInformation) - require.True(t, vendorInfo.Options.Has(OptionReplyPort)) + vendorOpts := GetVendorOptions(m.Options) + require.True(t, vendorOpts.Options.Has(OptionReplyPort)) - opt = vendorInfo.GetOneOption(OptionReplyPort) - require.Equal(t, replyPort, opt.(*OptReplyPort).Port) + port, err := GetReplyPort(vendorOpts.Options) + require.NoError(t, err) + require.Equal(t, replyPort, port) } func newAck(hwAddr net.HardwareAddr, transactionID [4]byte) *dhcpv4.DHCPv4 { @@ -108,7 +107,7 @@ func newAck(hwAddr net.HardwareAddr, transactionID [4]byte) *dhcpv4.DHCPv4 { ack.TransactionID = transactionID ack.HWType = iana.HWTypeEthernet ack.ClientHWAddr = hwAddr - ack.UpdateOption(&dhcpv4.OptMessageType{MessageType: dhcpv4.MessageTypeAck}) + ack.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) return ack } @@ -126,9 +125,9 @@ func TestInformSelectForAck_Broadcast(t *testing.T) { } ack := newAck(hwAddr, tid) ack.SetBroadcast() - ack.UpdateOption(&dhcpv4.OptServerIdentifier{ServerID: serverID}) + ack.UpdateOption(dhcpv4.OptServerIdentifier(serverID)) - m, err := InformSelectForAck(*ack, 0, bootImage) + m, err := InformSelectForAck(PacketFor(ack), 0, bootImage) require.NoError(t, err) require.Equal(t, dhcpv4.OpcodeBootRequest, m.OpCode) require.Equal(t, ack.HWType, m.HWType) @@ -140,17 +139,16 @@ func TestInformSelectForAck_Broadcast(t *testing.T) { require.True(t, m.Options.Has(dhcpv4.OptionClassIdentifier)) require.True(t, m.Options.Has(dhcpv4.OptionParameterRequestList)) require.True(t, m.Options.Has(dhcpv4.OptionDHCPMessageType)) - opt := m.GetOneOption(dhcpv4.OptionDHCPMessageType) - require.Equal(t, dhcpv4.MessageTypeInform, opt.(*dhcpv4.OptMessageType).MessageType) + mt := dhcpv4.GetMessageType(m.Options) + require.Equal(t, dhcpv4.MessageTypeInform, mt) // Validate vendor opts. require.True(t, m.Options.Has(dhcpv4.OptionVendorSpecificInformation)) - opt = m.GetOneOption(dhcpv4.OptionVendorSpecificInformation) - vendorInfo := opt.(*OptVendorSpecificInformation) - RequireHasOption(t, vendorInfo.Options, &OptMessageType{Type: MessageTypeSelect}) - require.True(t, vendorInfo.Options.Has(OptionVersion)) - RequireHasOption(t, vendorInfo.Options, &OptSelectedBootImageID{ID: bootImage.ID}) - RequireHasOption(t, vendorInfo.Options, &OptServerIdentifier{ServerID: serverID}) + vendorOpts := GetVendorOptions(m.Options).Options + RequireHasOption(t, vendorOpts, OptMessageType(MessageTypeSelect)) + require.True(t, vendorOpts.Has(OptionVersion)) + RequireHasOption(t, vendorOpts, OptSelectedBootImageID(bootImage.ID)) + RequireHasOption(t, vendorOpts, OptServerIdentifier(serverID)) } func TestInformSelectForAck_NoServerID(t *testing.T) { @@ -166,7 +164,7 @@ func TestInformSelectForAck_NoServerID(t *testing.T) { } ack := newAck(hwAddr, tid) - _, err := InformSelectForAck(*ack, 0, bootImage) + _, err := InformSelectForAck(PacketFor(ack), 0, bootImage) require.Error(t, err, "expect error for no server identifier option") } @@ -184,9 +182,9 @@ func TestInformSelectForAck_BadReplyPort(t *testing.T) { } ack := newAck(hwAddr, tid) ack.SetBroadcast() - ack.UpdateOption(&dhcpv4.OptServerIdentifier{ServerID: serverID}) + ack.UpdateOption(dhcpv4.OptServerIdentifier(serverID)) - _, err := InformSelectForAck(*ack, 11223, bootImage) + _, err := InformSelectForAck(PacketFor(ack), 11223, bootImage) require.Error(t, err, "expect error for > 1024 replyPort") } @@ -204,16 +202,15 @@ func TestInformSelectForAck_ReplyPort(t *testing.T) { } ack := newAck(hwAddr, tid) ack.SetBroadcast() - ack.UpdateOption(&dhcpv4.OptServerIdentifier{ServerID: serverID}) + ack.UpdateOption(dhcpv4.OptServerIdentifier(serverID)) replyPort := uint16(999) - m, err := InformSelectForAck(*ack, replyPort, bootImage) + m, err := InformSelectForAck(PacketFor(ack), replyPort, bootImage) require.NoError(t, err) require.True(t, m.Options.Has(dhcpv4.OptionVendorSpecificInformation)) - opt := m.GetOneOption(dhcpv4.OptionVendorSpecificInformation) - vendorInfo := opt.(*OptVendorSpecificInformation) - RequireHasOption(t, vendorInfo.Options, &OptReplyPort{Port: replyPort}) + vendorOpts := GetVendorOptions(m.Options).Options + RequireHasOption(t, vendorOpts, OptReplyPort(replyPort)) } func TestNewReplyForInformList_NoDefaultImage(t *testing.T) { @@ -274,24 +271,24 @@ func TestNewReplyForInformList(t *testing.T) { require.Equal(t, "bsdp.foo.com", ack.ServerHostName) // Validate options. - RequireHasOption(t, ack.Options, &dhcpv4.OptMessageType{MessageType: dhcpv4.MessageTypeAck}) - RequireHasOption(t, ack.Options, &dhcpv4.OptServerIdentifier{ServerID: net.IP{9, 9, 9, 9}}) - RequireHasOption(t, ack.Options, &dhcpv4.OptClassIdentifier{Identifier: AppleVendorID}) + RequireHasOption(t, ack.Options, dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) + RequireHasOption(t, ack.Options, dhcpv4.OptServerIdentifier(net.IP{9, 9, 9, 9})) + RequireHasOption(t, ack.Options, dhcpv4.OptClassIdentifier(AppleVendorID)) require.NotNil(t, ack.GetOneOption(dhcpv4.OptionVendorSpecificInformation)) // Vendor-specific options. - vendorOpts := ack.GetOneOption(dhcpv4.OptionVendorSpecificInformation).(*OptVendorSpecificInformation) - RequireHasOption(t, vendorOpts.Options, &OptMessageType{Type: MessageTypeList}) - RequireHasOption(t, vendorOpts.Options, &OptDefaultBootImageID{ID: images[0].ID}) - RequireHasOption(t, vendorOpts.Options, &OptServerPriority{Priority: 0x7070}) - RequireHasOption(t, vendorOpts.Options, &OptBootImageList{Images: images}) + vendorOpts := GetVendorOptions(ack.Options).Options + RequireHasOption(t, vendorOpts, OptMessageType(MessageTypeList)) + RequireHasOption(t, vendorOpts, OptDefaultBootImageID(images[0].ID)) + RequireHasOption(t, vendorOpts, OptServerPriority(0x7070)) + RequireHasOption(t, vendorOpts, OptBootImageList(images...)) // Add in selected boot image, ensure it's in the generated ACK. config.SelectedImage = &images[0] ack, err = NewReplyForInformList(inform, config) require.NoError(t, err) - vendorOpts = ack.GetOneOption(dhcpv4.OptionVendorSpecificInformation).(*OptVendorSpecificInformation) - RequireHasOption(t, vendorOpts.Options, &OptSelectedBootImageID{ID: images[0].ID}) + vendorOpts = GetVendorOptions(ack.Options).Options + RequireHasOption(t, vendorOpts, OptSelectedBootImageID(images[0].ID)) } func TestNewReplyForInformSelect_NoSelectedImage(t *testing.T) { @@ -352,30 +349,22 @@ func TestNewReplyForInformSelect(t *testing.T) { require.Equal(t, "bsdp.foo.com", ack.ServerHostName) // Validate options. - RequireHasOption(t, ack.Options, &dhcpv4.OptMessageType{MessageType: dhcpv4.MessageTypeAck}) - RequireHasOption(t, ack.Options, &dhcpv4.OptServerIdentifier{ServerID: net.IP{9, 9, 9, 9}}) - RequireHasOption(t, ack.Options, &dhcpv4.OptServerIdentifier{ServerID: net.IP{9, 9, 9, 9}}) - RequireHasOption(t, ack.Options, &dhcpv4.OptClassIdentifier{Identifier: AppleVendorID}) + RequireHasOption(t, ack.Options, dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) + RequireHasOption(t, ack.Options, dhcpv4.OptServerIdentifier(net.IP{9, 9, 9, 9})) + RequireHasOption(t, ack.Options, dhcpv4.OptServerIdentifier(net.IP{9, 9, 9, 9})) + RequireHasOption(t, ack.Options, dhcpv4.OptClassIdentifier(AppleVendorID)) require.NotNil(t, ack.GetOneOption(dhcpv4.OptionVendorSpecificInformation)) - vendorOpts := ack.GetOneOption(dhcpv4.OptionVendorSpecificInformation).(*OptVendorSpecificInformation) - RequireHasOption(t, vendorOpts.Options, &OptMessageType{Type: MessageTypeSelect}) - RequireHasOption(t, vendorOpts.Options, &OptSelectedBootImageID{ID: images[0].ID}) + vendorOpts := GetVendorOptions(ack.Options) + RequireHasOption(t, vendorOpts.Options, OptMessageType(MessageTypeSelect)) + RequireHasOption(t, vendorOpts.Options, OptSelectedBootImageID(images[0].ID)) } func TestMessageTypeForPacket(t *testing.T) { - var ( - pkt *dhcpv4.DHCPv4 - gotMessageType *MessageType - ) - - list := new(MessageType) - *list = MessageTypeList - testcases := []struct { tcName string opts []dhcpv4.Option - wantMessageType *MessageType + wantMessageType MessageType }{ { tcName: "No options", @@ -384,45 +373,38 @@ func TestMessageTypeForPacket(t *testing.T) { { tcName: "Some options, no vendor opts", opts: []dhcpv4.Option{ - &dhcpv4.OptHostName{HostName: "foobar1234"}, + dhcpv4.OptHostName("foobar1234"), }, }, { tcName: "Vendor opts, no message type", opts: []dhcpv4.Option{ - &dhcpv4.OptHostName{HostName: "foobar1234"}, - &OptVendorSpecificInformation{ - Options: []dhcpv4.Option{ - Version1_1, - }, - }, + dhcpv4.OptHostName("foobar1234"), + OptVendorOptions( + OptVersion(Version1_1), + ), }, }, { tcName: "Vendor opts, with message type", opts: []dhcpv4.Option{ - &dhcpv4.OptHostName{HostName: "foobar1234"}, - &OptVendorSpecificInformation{ - Options: []dhcpv4.Option{ - Version1_1, - &OptMessageType{Type: MessageTypeList}, - }, - }, + dhcpv4.OptHostName("foobar1234"), + OptVendorOptions( + OptVersion(Version1_1), + OptMessageType(MessageTypeList), + ), }, - wantMessageType: list, + wantMessageType: MessageTypeList, }, } for _, tt := range testcases { t.Run(tt.tcName, func(t *testing.T) { - pkt, _ = dhcpv4.New() + pkt, _ := dhcpv4.New() for _, opt := range tt.opts { pkt.UpdateOption(opt) } - gotMessageType = MessageTypeFromPacket(pkt) + gotMessageType := MessageTypeFromPacket(pkt) require.Equal(t, tt.wantMessageType, gotMessageType) - if tt.wantMessageType != nil { - require.Equal(t, *tt.wantMessageType, *gotMessageType) - } }) } } diff --git a/dhcpv4/bsdp/client.go b/dhcpv4/bsdp/client.go index dd4a0a0..e8ca2ca 100644 --- a/dhcpv4/bsdp/client.go +++ b/dhcpv4/bsdp/client.go @@ -18,24 +18,10 @@ func NewClient() *Client { return &Client{Client: dhcpv4.Client{}} } -func castVendorOpt(ack *dhcpv4.DHCPv4) { - opts := ack.Options - for i := 0; i < len(opts); i++ { - if opts[i].Code() == dhcpv4.OptionVendorSpecificInformation { - vendorOpt, err := ParseOptVendorSpecificInformation(opts[i].ToBytes()) - // Oh well, we tried - if err != nil { - return - } - opts[i] = vendorOpt - } - } -} - // Exchange runs a full BSDP exchange (Inform[list], Ack, Inform[select], // Ack). Returns a list of DHCPv4 structures representing the exchange. -func (c *Client) Exchange(ifname string) ([]*dhcpv4.DHCPv4, error) { - conversation := make([]*dhcpv4.DHCPv4, 0) +func (c *Client) Exchange(ifname string) ([]*Packet, error) { + conversation := make([]*Packet, 0) // Get our file descriptor for the broadcast socket. sendFd, err := dhcpv4.MakeBroadcastSocket(ifname) @@ -55,17 +41,16 @@ func (c *Client) Exchange(ifname string) ([]*dhcpv4.DHCPv4, error) { conversation = append(conversation, informList) // ACK[LIST] - ackForList, err := c.Client.SendReceive(sendFd, recvFd, informList, dhcpv4.MessageTypeAck) + ackForList, err := c.Client.SendReceive(sendFd, recvFd, informList.v4(), dhcpv4.MessageTypeAck) if err != nil { return conversation, err } // Rewrite vendor-specific option for pretty printing. - castVendorOpt(ackForList) - conversation = append(conversation, ackForList) + conversation = append(conversation, PacketFor(ackForList)) // Parse boot images sent back by server - bootImages, err := ParseBootImageListFromAck(*ackForList) + bootImages, err := ParseBootImageListFromAck(ackForList) if err != nil { return conversation, err } @@ -74,17 +59,16 @@ func (c *Client) Exchange(ifname string) ([]*dhcpv4.DHCPv4, error) { } // INFORM[SELECT] - informSelect, err := InformSelectForAck(*ackForList, dhcpv4.ClientPort, bootImages[0]) + informSelect, err := InformSelectForAck(PacketFor(ackForList), dhcpv4.ClientPort, bootImages[0]) if err != nil { return conversation, err } conversation = append(conversation, informSelect) // ACK[SELECT] - ackForSelect, err := c.Client.SendReceive(sendFd, recvFd, informSelect, dhcpv4.MessageTypeAck) - castVendorOpt(ackForSelect) + ackForSelect, err := c.Client.SendReceive(sendFd, recvFd, informSelect.v4(), dhcpv4.MessageTypeAck) if err != nil { return conversation, err } - return append(conversation, ackForSelect), nil + return append(conversation, PacketFor(ackForSelect)), nil } diff --git a/dhcpv4/bsdp/option_vendor_specific_information.go b/dhcpv4/bsdp/option_vendor_specific_information.go index a87135f..4e107e1 100644 --- a/dhcpv4/bsdp/option_vendor_specific_information.go +++ b/dhcpv4/bsdp/option_vendor_specific_information.go @@ -1,93 +1,87 @@ package bsdp import ( - "strings" + "fmt" "github.com/insomniacslk/dhcp/dhcpv4" - "github.com/u-root/u-root/pkg/uio" ) -// OptVendorSpecificInformation encapsulates the BSDP-specific options used for -// the protocol. -type OptVendorSpecificInformation struct { - Options dhcpv4.Options +// VendorOptions is like dhcpv4.Options, but stringifies using BSDP-specific +// option codes. +type VendorOptions struct { + dhcpv4.Options } -// parseOption is similar to dhcpv4.ParseOption, except that it switches based -// on the BSDP specific options. -func parseOption(code dhcpv4.OptionCode, data []byte) (dhcpv4.Option, error) { - var ( - opt dhcpv4.Option - err error - ) - switch code { - case OptionBootImageList: - opt, err = ParseOptBootImageList(data) - case OptionDefaultBootImageID: - opt, err = ParseOptDefaultBootImageID(data) - case OptionMachineName: - opt, err = ParseOptMachineName(data) - case OptionMessageType: - opt, err = ParseOptMessageType(data) - case OptionReplyPort: - opt, err = ParseOptReplyPort(data) - case OptionSelectedBootImageID: - opt, err = ParseOptSelectedBootImageID(data) - case OptionServerIdentifier: - opt, err = ParseOptServerIdentifier(data) - case OptionServerPriority: - opt, err = ParseOptServerPriority(data) - case OptionVersion: - opt, err = ParseOptVersion(data) - default: - opt, err = ParseOptGeneric(code, data) - } - if err != nil { - return nil, err - } - return opt, nil +// String prints the contained options using BSDP-specific option code parsing. +func (v VendorOptions) String() string { + return v.Options.ToString(bsdpHumanizer) } -// codeGetter is a dhcpv4.OptionCodeGetter for BSDP optionCodes. -func codeGetter(c uint8) dhcpv4.OptionCode { - return optionCode(c) +// FromBytes parses vendor options from +func (v *VendorOptions) FromBytes(data []byte) error { + v.Options = make(dhcpv4.Options) + return v.Options.FromBytes(data) } -// ParseOptVendorSpecificInformation constructs an OptVendorSpecificInformation struct from a sequence of -// bytes and returns it, or an error. -func ParseOptVendorSpecificInformation(data []byte) (*OptVendorSpecificInformation, error) { - options, err := dhcpv4.OptionsFromBytesWithParser(data, codeGetter, parseOption, false /* don't check for OptionEnd tag */) - if err != nil { - return nil, err +// OptVendorOptions returns the BSDP Vendor Specific Info in o. +func OptVendorOptions(o ...dhcpv4.Option) dhcpv4.Option { + return dhcpv4.Option{ + Code: dhcpv4.OptionVendorSpecificInformation, + Value: VendorOptions{dhcpv4.OptionsFromList(o...)}, } - return &OptVendorSpecificInformation{options}, nil } -// Code returns the option code. -func (o *OptVendorSpecificInformation) Code() dhcpv4.OptionCode { - return dhcpv4.OptionVendorSpecificInformation +// GetVendorOptions returns a new BSDP Vendor Specific Info option. +func GetVendorOptions(o dhcpv4.Options) *VendorOptions { + v := o.Get(dhcpv4.OptionVendorSpecificInformation) + if v == nil { + return nil + } + var vo VendorOptions + if err := vo.FromBytes(v); err != nil { + return nil + } + return &vo } -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptVendorSpecificInformation) ToBytes() []byte { - return uio.ToBigEndian(o.Options) +var bsdpHumanizer = dhcpv4.OptionHumanizer{ + ValueHumanizer: parseOption, + CodeHumanizer: func(c uint8) dhcpv4.OptionCode { + return optionCode(c) + }, } -// String returns a human-readable string for this option. -func (o *OptVendorSpecificInformation) String() string { - s := "Vendor Specific Information ->" - for _, opt := range o.Options { - optString := opt.String() - // If this option has sub-structures, offset them accordingly. - if strings.Contains(optString, "\n") { - optString = strings.Replace(optString, "\n ", "\n ", -1) - } - s += "\n " + optString - } - return s -} +// parseOption is similar to dhcpv4.parseOption, except that it interprets +// option codes based on the BSDP-specific options. +func parseOption(code dhcpv4.OptionCode, data []byte) fmt.Stringer { + var d dhcpv4.OptionDecoder + switch code { + case OptionMachineName: + var s dhcpv4.String + d = &s + + case OptionServerIdentifier: + d = &dhcpv4.IP{} + + case OptionServerPriority, OptionReplyPort: + var u dhcpv4.Uint16 + d = &u + + case OptionBootImageList: + d = &BootImageList{} -// GetOneOption returns the first suboption that matches the OptionCode code. -func (o *OptVendorSpecificInformation) GetOneOption(code dhcpv4.OptionCode) dhcpv4.Option { - return o.Options.GetOne(code) + case OptionDefaultBootImageID, OptionSelectedBootImageID: + d = &BootImageID{} + + case OptionMessageType: + var m MessageType + d = &m + + case OptionVersion: + d = &Version{} + } + if d != nil && d.FromBytes(data) == nil { + return d + } + return dhcpv4.OptionGeneric{data} } diff --git a/dhcpv4/bsdp/option_vendor_specific_information_test.go b/dhcpv4/bsdp/option_vendor_specific_information_test.go index ede8a0b..a6727f5 100644 --- a/dhcpv4/bsdp/option_vendor_specific_information_test.go +++ b/dhcpv4/bsdp/option_vendor_specific_information_test.go @@ -1,6 +1,7 @@ package bsdp import ( + "net" "testing" "github.com/insomniacslk/dhcp/dhcpv4" @@ -8,182 +9,71 @@ import ( ) func TestOptVendorSpecificInformationInterfaceMethods(t *testing.T) { - messageTypeOpt := &OptMessageType{MessageTypeList} - versionOpt := Version1_1 - o := &OptVendorSpecificInformation{[]dhcpv4.Option{messageTypeOpt, versionOpt}} - require.Equal(t, dhcpv4.OptionVendorSpecificInformation, o.Code(), "Code") - - expectedBytes := []byte{ - 1, 1, 1, // List option - 2, 2, 1, 1, // Version option - } - o = &OptVendorSpecificInformation{ - []dhcpv4.Option{ - &OptMessageType{MessageTypeList}, - Version1_1, - }, - } - require.Equal(t, expectedBytes, o.ToBytes(), "ToBytes") -} - -func TestParseOptVendorSpecificInformation(t *testing.T) { - var ( - o *OptVendorSpecificInformation - err error + o := OptVendorOptions( + OptVersion(Version1_1), + OptMessageType(MessageTypeList), ) - o, err = ParseOptVendorSpecificInformation([]byte{1, 2}) - require.Error(t, err, "short byte stream") - - // Good byte stream - data := []byte{ - 1, 1, 1, // List option - 2, 2, 1, 1, // Version option - } - o, err = ParseOptVendorSpecificInformation(data) - require.NoError(t, err) - expected := &OptVendorSpecificInformation{ - []dhcpv4.Option{ - &OptMessageType{MessageTypeList}, - Version1_1, - }, - } - require.Equal(t, 2, len(o.Options), "number of parsed suboptions") - typ := o.GetOneOption(OptionMessageType) - version := o.GetOneOption(OptionVersion) - require.Equal(t, expected.Options[0].Code(), typ.Code()) - require.Equal(t, expected.Options[1].Code(), version.Code()) - - // Short byte stream (length and data mismatch) - data = []byte{ - 1, 1, 1, // List option - 2, 2, 1, // Version option - } - o, err = ParseOptVendorSpecificInformation(data) - require.Error(t, err) - - // Bad option - data = []byte{ - 1, 1, 1, // List option - 2, 2, 1, // Version option - 5, 3, 1, 1, 1, // Reply port option - } - o, err = ParseOptVendorSpecificInformation(data) - require.Error(t, err) + require.Equal(t, dhcpv4.OptionVendorSpecificInformation, o.Code, "Code") - // Boot images + default. - data = []byte{ + expectedBytes := []byte{ 1, 1, 1, // List option 2, 2, 1, 1, // Version option - 5, 2, 1, 1, // Reply port option - - // Boot image list - 9, 22, - 0x1, 0x0, 0x03, 0xe9, // ID - 6, // name length - 'b', 's', 'd', 'p', '-', '1', - 0x80, 0x0, 0x23, 0x31, // ID - 6, // name length - 'b', 's', 'd', 'p', '-', '2', - - // Default Boot Image ID - 7, 4, 0x1, 0x0, 0x03, 0xe9, } - o, err = ParseOptVendorSpecificInformation(data) - require.NoError(t, err) - require.Equal(t, 5, len(o.Options)) - for _, opt := range []dhcpv4.OptionCode{ - OptionMessageType, - OptionVersion, - OptionReplyPort, - OptionBootImageList, - OptionDefaultBootImageID, - } { - require.True(t, o.Options.Has(opt)) - } - optBootImage := o.GetOneOption(OptionBootImageList).(*OptBootImageList) - expectedBootImages := []BootImage{ - BootImage{ - ID: BootImageID{ - IsInstall: false, - ImageType: BootImageTypeMacOSX, - Index: 1001, - }, - Name: "bsdp-1", - }, - BootImage{ - ID: BootImageID{ - IsInstall: true, - ImageType: BootImageTypeMacOS9, - Index: 9009, - }, - Name: "bsdp-2", - }, - } - require.Equal(t, expectedBootImages, optBootImage.Images) + require.Equal(t, expectedBytes, o.Value.ToBytes(), "ToBytes") } func TestOptVendorSpecificInformationString(t *testing.T) { - o := &OptVendorSpecificInformation{ - []dhcpv4.Option{ - &OptMessageType{MessageTypeList}, - Version1_1, - }, - } - expectedString := "Vendor Specific Information ->\n BSDP Message Type -> LIST\n BSDP Version -> 1.1" + o := OptVendorOptions( + OptMessageType(MessageTypeList), + OptVersion(Version1_1), + ) + expectedString := "Vendor Specific Information:\n BSDP Message Type: LIST\n BSDP Version: 1.1\n" require.Equal(t, expectedString, o.String()) // Test more complicated string - sub options of sub options. - o = &OptVendorSpecificInformation{ - []dhcpv4.Option{ - &OptMessageType{MessageTypeList}, - &OptBootImageList{ - []BootImage{ - BootImage{ - ID: BootImageID{ - IsInstall: false, - ImageType: BootImageTypeMacOSX, - Index: 1001, - }, - Name: "bsdp-1", - }, - BootImage{ - ID: BootImageID{ - IsInstall: true, - ImageType: BootImageTypeMacOS9, - Index: 9009, - }, - Name: "bsdp-2", - }, + o = OptVendorOptions( + OptMessageType(MessageTypeList), + OptBootImageList( + BootImage{ + ID: BootImageID{ + IsInstall: false, + ImageType: BootImageTypeMacOSX, + Index: 1001, }, + Name: "bsdp-1", }, - }, - } - expectedString = "Vendor Specific Information ->\n" + - " BSDP Message Type -> LIST\n" + - " BSDP Boot Image List ->\n" + - " bsdp-1 [1001] uninstallable macOS image\n" + - " bsdp-2 [9009] installable macOS 9 image" + BootImage{ + ID: BootImageID{ + IsInstall: true, + ImageType: BootImageTypeMacOS9, + Index: 9009, + }, + Name: "bsdp-2", + }, + ), + OptMachineName("foo"), + OptServerIdentifier(net.IP{1, 1, 1, 1}), + OptServerPriority(1234), + OptReplyPort(1235), + OptDefaultBootImageID(BootImageID{ + IsInstall: true, + ImageType: BootImageTypeMacOS9, + Index: 9009, + }), + OptSelectedBootImageID(BootImageID{ + IsInstall: true, + ImageType: BootImageTypeMacOS9, + Index: 9009, + }), + ) + expectedString = "Vendor Specific Information:\n" + + " BSDP Message Type: LIST\n" + + " BSDP Server Identifier: 1.1.1.1\n" + + " BSDP Server Priority: 1234\n" + + " BSDP Reply Port: 1235\n" + + " BSDP Default Boot Image ID: [9009] installable macOS 9 image\n" + + " BSDP Selected Boot Image ID: [9009] installable macOS 9 image\n" + + " BSDP Boot Image List: bsdp-1 [1001] uninstallable macOS image, bsdp-2 [9009] installable macOS 9 image\n" + + " BSDP Machine Name: foo\n" require.Equal(t, expectedString, o.String()) } - -func TestOptVendorSpecificInformationGetOneOption(t *testing.T) { - // No option - o := &OptVendorSpecificInformation{ - []dhcpv4.Option{ - &OptMessageType{MessageTypeList}, - Version1_1, - }, - } - foundOpt := o.GetOneOption(OptionBootImageList) - require.Nil(t, foundOpt, "should not get options") - - // One option - o = &OptVendorSpecificInformation{ - []dhcpv4.Option{ - &OptMessageType{MessageTypeList}, - Version1_1, - }, - } - foundOpt = o.GetOneOption(OptionMessageType) - require.Equal(t, MessageTypeList, foundOpt.(*OptMessageType).Type) -} diff --git a/dhcpv4/bsdp/types.go b/dhcpv4/bsdp/types.go index 4ce840f..4931081 100644 --- a/dhcpv4/bsdp/types.go +++ b/dhcpv4/bsdp/types.go @@ -1,5 +1,9 @@ package bsdp +import ( + "fmt" +) + // DefaultMacOSVendorClassIdentifier is a default vendor class identifier used // on non-darwin hosts where the vendor class identifier cannot be determined. // It should mostly be used for debugging if testing BSDP on a non-darwin @@ -19,7 +23,7 @@ func (o optionCode) String() string { if s, ok := optionCodeToString[o]; ok { return s } - return "unknown" + return fmt.Sprintf("unknown (%d)", o) } // Options (occur as sub-options of DHCP option 43). diff --git a/dhcpv4/dhcpv4.go b/dhcpv4/dhcpv4.go index bbb2f37..d94c58a 100644 --- a/dhcpv4/dhcpv4.go +++ b/dhcpv4/dhcpv4.go @@ -1,3 +1,18 @@ +// Package dhcpv4 provides encoding and decoding of DHCPv4 packets and options. +// +// Example Usage: +// +// p, err := dhcpv4.New( +// dhcpv4.WithClientIP(net.IP{192, 168, 0, 1}), +// dhcpv4.WithMessageType(dhcpv4.MessageTypeInform), +// ) +// p.UpdateOption(dhcpv4.OptServerIdentifier(net.IP{192, 110, 110, 110})) +// +// // Retrieve the DHCP Message Type option. +// m := dhcpv4.GetMessageType(p.Options) +// +// bytesOnTheWire := p.ToBytes() +// longSummary := p.Summary() package dhcpv4 import ( @@ -121,7 +136,7 @@ func New(modifiers ...Modifier) (*DHCPv4, error) { YourIPAddr: net.IPv4zero, ServerIPAddr: net.IPv4zero, GatewayIPAddr: net.IPv4zero, - Options: make([]Option, 0, 10), + Options: make(Options), } for _, mod := range modifiers { mod(&d) @@ -203,11 +218,7 @@ func NewInform(hwaddr net.HardwareAddr, localIP net.IP, modifiers ...Modifier) ( // NewRequestFromOffer builds a DHCPv4 request from an offer. func NewRequestFromOffer(offer *DHCPv4, modifiers ...Modifier) (*DHCPv4, error) { // find server IP address - var serverIP net.IP - serverID := offer.GetOneOption(OptionServerIdentifier) - if serverID != nil { - serverIP = serverID.(*OptServerIdentifier).ServerID - } + serverIP := GetServerIdentifier(offer.Options) if serverIP == nil { return nil, errors.New("Missing Server IP Address in DHCP Offer") } @@ -216,8 +227,8 @@ func NewRequestFromOffer(offer *DHCPv4, modifiers ...Modifier) (*DHCPv4, error) WithReply(offer), WithMessageType(MessageTypeRequest), WithServerIP(serverIP), - WithOption(&OptRequestedIPAddress{RequestedAddr: offer.YourIPAddr}), - WithOption(&OptServerIdentifier{ServerID: serverIP}), + WithOption(OptRequestedIPAddress(offer.YourIPAddr)), + WithOption(OptServerIdentifier(serverIP)), )...) } @@ -281,11 +292,10 @@ func FromBytes(q []byte) (*DHCPv4, error) { return nil, fmt.Errorf("malformed DHCP packet: got magic cookie %v, want %v", cookie[:], magicCookie[:]) } - opts, err := OptionsFromBytes(buf.Data()) - if err != nil { + p.Options = make(Options) + if err := p.Options.fromBytesCheckEnd(buf.Data(), true); err != nil { return nil, err } - p.Options = opts return &p, nil } @@ -325,25 +335,25 @@ func (d *DHCPv4) SetUnicast() { // GetOneOption returns the option that matches the given option code. // -// If no matching option is found, nil is returned. -func (d *DHCPv4) GetOneOption(code OptionCode) Option { - return d.Options.GetOne(code) +// According to RFC 3396, options that are specified more than once are +// concatenated, and hence this should always just return one option. +func (d *DHCPv4) GetOneOption(code OptionCode) []byte { + return d.Options.Get(code) } // UpdateOption replaces an existing option with the same option code with the // given one, adding it if not already present. -func (d *DHCPv4) UpdateOption(option Option) { - d.Options.Update(option) +func (d *DHCPv4) UpdateOption(opt Option) { + if d.Options == nil { + d.Options = make(Options) + } + d.Options.Update(opt) } // MessageType returns the message type, trying to extract it from the // OptMessageType option. It returns nil if the message type cannot be extracted func (d *DHCPv4) MessageType() MessageType { - opt := d.GetOneOption(OptionDHCPMessageType) - if opt == nil { - return MessageTypeNone - } - return opt.(*OptMessageType).MessageType + return GetMessageType(d.Options) } // String implements fmt.Stringer. @@ -352,23 +362,24 @@ func (d *DHCPv4) String() string { d.OpCode, d.TransactionID, d.HWType, d.ClientHWAddr) } -// Summary prints detailed information about the packet. -func (d *DHCPv4) Summary() string { +// SummaryWithVendor prints a summary of the packet, interpreting the +// vendor-specific info option using the given parser (can be nil). +func (d *DHCPv4) SummaryWithVendor(vendorDecoder OptionDecoder) string { ret := fmt.Sprintf( - "DHCPv4\n"+ - " opcode=%s\n"+ - " hwtype=%s\n"+ - " hopcount=%v\n"+ - " transactionid=%s\n"+ - " numseconds=%v\n"+ - " flags=%v (0x%02x)\n"+ - " clientipaddr=%s\n"+ - " youripaddr=%s\n"+ - " serveripaddr=%s\n"+ - " gatewayipaddr=%s\n"+ - " clienthwaddr=%s\n"+ - " serverhostname=%s\n"+ - " bootfilename=%s\n", + "DHCPv4 Message\n"+ + " opcode: %s\n"+ + " hwtype: %s\n"+ + " hopcount: %v\n"+ + " transaction ID: %s\n"+ + " num seconds: %v\n"+ + " flags: %v (0x%02x)\n"+ + " client IP: %s\n"+ + " your IP: %s\n"+ + " server IP: %s\n"+ + " gateway IP: %s\n"+ + " client MAC: %s\n"+ + " server hostname: %s\n"+ + " bootfile name: %s\n", d.OpCode, d.HWType, d.HopCount, @@ -384,29 +395,20 @@ func (d *DHCPv4) Summary() string { d.ServerHostName, d.BootFileName, ) - ret += " options=\n" - for _, opt := range d.Options { - optString := opt.String() - // If this option has sub structures, offset them accordingly. - if strings.Contains(optString, "\n") { - optString = strings.Replace(optString, "\n ", "\n ", -1) - } - ret += fmt.Sprintf(" %v\n", optString) - if opt.Code() == OptionEnd { - break - } - } + ret += " options:\n" + ret += d.Options.Summary(vendorDecoder) return ret } +// Summary prints detailed information about the packet. +func (d *DHCPv4) Summary() string { + return d.SummaryWithVendor(nil) +} + // IsOptionRequested returns true if that option is within the requested // options of the DHCPv4 message. func (d *DHCPv4) IsOptionRequested(requested OptionCode) bool { - optprl := d.GetOneOption(OptionParameterRequestList) - if optprl == nil { - return false - } - for _, o := range optprl.(*OptParameterRequestList).RequestedOpts { + for _, o := range GetParameterRequestList(d.Options) { if o == requested { return true } @@ -459,7 +461,12 @@ func (d *DHCPv4) ToBytes() []byte { // The magic cookie. buf.WriteBytes(magicCookie[:]) + + // Write all options. d.Options.Marshal(buf) + + // Finish the packet. buf.Write8(uint8(OptionEnd)) + return buf.Data() } diff --git a/dhcpv4/dhcpv4_test.go b/dhcpv4/dhcpv4_test.go index fb0ef70..a3e2a8a 100644 --- a/dhcpv4/dhcpv4_test.go +++ b/dhcpv4/dhcpv4_test.go @@ -179,43 +179,43 @@ func TestGetOption(t *testing.T) { t.Fatal(err) } - hostnameOpt := &OptionGeneric{OptionCode: OptionHostName, Data: []byte("darkstar")} - bootFileOpt := &OptBootfileName{"boot.img"} + hostnameOpt := OptGeneric(OptionHostName, []byte("darkstar")) + bootFileOpt := OptBootFileName("boot.img") d.UpdateOption(hostnameOpt) d.UpdateOption(bootFileOpt) - require.Equal(t, d.GetOneOption(OptionHostName), hostnameOpt) - require.Equal(t, d.GetOneOption(OptionBootfileName), bootFileOpt) - require.Equal(t, d.GetOneOption(OptionRouter), nil) + require.Equal(t, d.GetOneOption(OptionHostName), []byte("darkstar")) + require.Equal(t, d.GetOneOption(OptionBootfileName), []byte("boot.img")) + require.Equal(t, d.GetOneOption(OptionRouter), []byte(nil)) } func TestUpdateOption(t *testing.T) { d, err := New() require.NoError(t, err) - hostnameOpt := &OptionGeneric{OptionCode: OptionHostName, Data: []byte("darkstar")} - bootFileOpt1 := &OptBootfileName{"boot.img"} - bootFileOpt2 := &OptBootfileName{"boot2.img"} + hostnameOpt := OptGeneric(OptionHostName, []byte("darkstar")) + bootFileOpt1 := OptBootFileName("boot.img") + bootFileOpt2 := OptBootFileName("boot2.img") d.UpdateOption(hostnameOpt) d.UpdateOption(bootFileOpt1) d.UpdateOption(bootFileOpt2) options := d.Options require.Equal(t, len(options), 2) - require.Equal(t, d.Options.GetOne(OptionBootfileName), bootFileOpt2) - require.Equal(t, d.Options.GetOne(OptionHostName), hostnameOpt) + require.Equal(t, d.GetOneOption(OptionHostName), []byte("darkstar")) + require.Equal(t, d.GetOneOption(OptionBootfileName), []byte("boot2.img")) } func TestDHCPv4NewRequestFromOffer(t *testing.T) { offer, err := New() require.NoError(t, err) offer.SetBroadcast() - offer.UpdateOption(&OptMessageType{MessageType: MessageTypeOffer}) + offer.UpdateOption(OptMessageType(MessageTypeOffer)) req, err := NewRequestFromOffer(offer) require.Error(t, err) // Now add the option so it doesn't error out. - offer.UpdateOption(&OptServerIdentifier{ServerID: net.IPv4(192, 168, 0, 1)}) + offer.UpdateOption(OptServerIdentifier(net.IPv4(192, 168, 0, 1))) // Broadcast request req, err = NewRequestFromOffer(offer) @@ -235,13 +235,12 @@ func TestDHCPv4NewRequestFromOffer(t *testing.T) { func TestDHCPv4NewRequestFromOfferWithModifier(t *testing.T) { offer, err := New() require.NoError(t, err) - offer.UpdateOption(&OptMessageType{MessageType: MessageTypeOffer}) - offer.UpdateOption(&OptServerIdentifier{ServerID: net.IPv4(192, 168, 0, 1)}) + offer.UpdateOption(OptMessageType(MessageTypeOffer)) + offer.UpdateOption(OptServerIdentifier(net.IPv4(192, 168, 0, 1))) userClass := WithUserClass([]byte("linuxboot"), false) req, err := NewRequestFromOffer(offer, userClass) require.NoError(t, err) require.Equal(t, MessageTypeRequest, req.MessageType()) - require.Equal(t, "User Class Information -> linuxboot", req.Options[3].String()) } func TestNewReplyFromRequest(t *testing.T) { @@ -263,7 +262,6 @@ func TestNewReplyFromRequestWithModifier(t *testing.T) { require.NoError(t, err) require.Equal(t, discover.TransactionID, reply.TransactionID) require.Equal(t, discover.GatewayIPAddr, reply.GatewayIPAddr) - require.Equal(t, "User Class Information -> linuxboot", reply.Options[0].String()) } func TestDHCPv4MessageTypeNil(t *testing.T) { @@ -304,10 +302,33 @@ func TestIsOptionRequested(t *testing.T) { require.NoError(t, err) require.False(t, pkt.IsOptionRequested(OptionDomainNameServer)) - optprl := OptParameterRequestList{RequestedOpts: []OptionCode{OptionDomainNameServer}} - pkt.UpdateOption(&optprl) + optprl := OptParameterRequestList(OptionDomainNameServer) + pkt.UpdateOption(optprl) require.True(t, pkt.IsOptionRequested(OptionDomainNameServer)) } // TODO // test Summary() and String() +func TestSummary(t *testing.T) { + packet, err := New(WithMessageType(MessageTypeInform)) + packet.TransactionID = [4]byte{1, 1, 1, 1} + require.NoError(t, err) + + want := "DHCPv4 Message\n" + + " opcode: BootRequest\n" + + " hwtype: Ethernet\n" + + " hopcount: 0\n" + + " transaction ID: 0x01010101\n" + + " num seconds: 0\n" + + " flags: Unicast (0x00)\n" + + " client IP: 0.0.0.0\n" + + " your IP: 0.0.0.0\n" + + " server IP: 0.0.0.0\n" + + " gateway IP: 0.0.0.0\n" + + " client MAC: \n" + + " server hostname: \n" + + " bootfile name: \n" + + " options:\n" + + " DHCP Message Type: INFORM\n" + require.Equal(t, want, packet.Summary()) +} diff --git a/dhcpv4/modifiers.go b/dhcpv4/modifiers.go index 0759491..431fdfd 100644 --- a/dhcpv4/modifiers.go +++ b/dhcpv4/modifiers.go @@ -2,6 +2,7 @@ package dhcpv4 import ( "net" + "time" "github.com/insomniacslk/dhcp/iana" "github.com/insomniacslk/dhcp/rfc1035label" @@ -89,10 +90,13 @@ func WithOption(opt Option) Modifier { // rfc compliant or not. More details in issue #113 func WithUserClass(uc []byte, rfc bool) Modifier { // TODO let the user specify multiple user classes - return WithOption(&OptUserClass{ - UserClasses: [][]byte{uc}, - Rfc3004: rfc, - }) + return func(d *DHCPv4) { + if rfc { + d.UpdateOption(OptRFC3004UserClass([][]byte{uc})) + } else { + d.UpdateOption(OptUserClass(uc)) + } + } } // WithNetboot adds bootfile URL and bootfile param options to a DHCPv4 packet. @@ -102,7 +106,7 @@ func WithNetboot(d *DHCPv4) { // WithMessageType adds the DHCPv4 message type m to a packet. func WithMessageType(m MessageType) Modifier { - return WithOption(&OptMessageType{m}) + return WithOption(OptMessageType(m)) } // WithRequestedOptions adds requested options to the packet. @@ -110,10 +114,11 @@ func WithRequestedOptions(optionCodes ...OptionCode) Modifier { return func(d *DHCPv4) { params := d.GetOneOption(OptionParameterRequestList) if params == nil { - d.UpdateOption(&OptParameterRequestList{OptionCodeList(optionCodes)}) + d.UpdateOption(OptParameterRequestList(optionCodes...)) } else { - opts := params.(*OptParameterRequestList) - opts.RequestedOpts.Add(optionCodes...) + cl := OptionCodeList(GetParameterRequestList(d.Options)) + cl.Add(optionCodes...) + d.UpdateOption(OptParameterRequestList(cl...)) } } } @@ -124,33 +129,23 @@ func WithRelay(ip net.IP) Modifier { return func(d *DHCPv4) { d.SetUnicast() d.GatewayIPAddr = ip - d.HopCount += 1 + d.HopCount++ } } // WithNetmask adds or updates an OptSubnetMask func WithNetmask(mask net.IPMask) Modifier { - return WithOption(&OptSubnetMask{SubnetMask: mask}) + return WithOption(OptSubnetMask(mask)) } // WithLeaseTime adds or updates an OptIPAddressLeaseTime func WithLeaseTime(leaseTime uint32) Modifier { - return WithOption(&OptIPAddressLeaseTime{LeaseTime: leaseTime}) -} - -// WithDNS adds or updates an OptionDomainNameServer -func WithDNS(dnses ...net.IP) Modifier { - return WithOption(&OptDomainNameServer{NameServers: dnses}) + return WithOption(OptIPAddressLeaseTime(time.Duration(leaseTime) * time.Second)) } // WithDomainSearchList adds or updates an OptionDomainSearch func WithDomainSearchList(searchList ...string) Modifier { - return WithOption(&OptDomainSearch{DomainSearch: &rfc1035label.Labels{ + return WithOption(OptDomainSearch(&rfc1035label.Labels{ Labels: searchList, - }}) -} - -// WithRouter adds or updates an OptionRouter -func WithRouter(routers ...net.IP) Modifier { - return WithOption(&OptRouter{Routers: routers}) + })) } diff --git a/dhcpv4/modifiers_test.go b/dhcpv4/modifiers_test.go index 2cac2a0..6233a7d 100644 --- a/dhcpv4/modifiers_test.go +++ b/dhcpv4/modifiers_test.go @@ -3,6 +3,7 @@ package dhcpv4 import ( "net" "testing" + "time" "github.com/stretchr/testify/require" ) @@ -35,13 +36,12 @@ func TestHwAddrModifier(t *testing.T) { } func TestWithOptionModifier(t *testing.T) { - d, err := New(WithOption(&OptDomainName{DomainName: "slackware.it"})) + d, err := New(WithOption(OptDomainName("slackware.it"))) require.NoError(t, err) - opt := d.GetOneOption(OptionDomainName) - require.NotNil(t, opt) - dnOpt := opt.(*OptDomainName) - require.Equal(t, "slackware.it", dnOpt.DomainName) + dnOpt := GetDomainName(d.Options) + require.NotNil(t, dnOpt) + require.Equal(t, "slackware.it", dnOpt) } func TestUserClassModifier(t *testing.T) { @@ -51,8 +51,7 @@ func TestUserClassModifier(t *testing.T) { expected := []byte{ 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', } - require.Equal(t, "User Class Information -> linuxboot", d.Options[0].String()) - require.Equal(t, expected, d.Options[0].ToBytes()) + require.Equal(t, expected, d.GetOneOption(OptionUserClassInformation)) } func TestUserClassModifierRFC(t *testing.T) { @@ -62,43 +61,35 @@ func TestUserClassModifierRFC(t *testing.T) { expected := []byte{ 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', } - require.Equal(t, "User Class Information -> linuxboot", d.Options[0].String()) - require.Equal(t, expected, d.Options[0].ToBytes()) + require.Equal(t, expected, d.GetOneOption(OptionUserClassInformation)) } func TestWithNetboot(t *testing.T) { d, err := New(WithNetboot) require.NoError(t, err) - require.Equal(t, "Parameter Request List -> TFTP Server Name, Bootfile Name", d.Options[0].String()) + require.Equal(t, "TFTP Server Name, Bootfile Name", GetParameterRequestList(d.Options).String()) } func TestWithNetbootExistingTFTP(t *testing.T) { - d, err := New() - require.NoError(t, err) - d.UpdateOption(&OptParameterRequestList{ - RequestedOpts: []OptionCode{OptionTFTPServerName}, - }) + d, _ := New() + d.UpdateOption(OptParameterRequestList(OptionTFTPServerName)) WithNetboot(d) - require.Equal(t, "Parameter Request List -> TFTP Server Name, Bootfile Name", d.Options[0].String()) + require.Equal(t, "TFTP Server Name, Bootfile Name", GetParameterRequestList(d.Options).String()) } func TestWithNetbootExistingBootfileName(t *testing.T) { d, _ := New() - d.UpdateOption(&OptParameterRequestList{ - RequestedOpts: []OptionCode{OptionBootfileName}, - }) + d.UpdateOption(OptParameterRequestList(OptionBootfileName)) WithNetboot(d) - require.Equal(t, "Parameter Request List -> Bootfile Name, TFTP Server Name", d.Options[0].String()) + require.Equal(t, "TFTP Server Name, Bootfile Name", GetParameterRequestList(d.Options).String()) } func TestWithNetbootExistingBoth(t *testing.T) { d, _ := New() - d.UpdateOption(&OptParameterRequestList{ - RequestedOpts: []OptionCode{OptionBootfileName, OptionTFTPServerName}, - }) + d.UpdateOption(OptParameterRequestList(OptionBootfileName, OptionTFTPServerName)) WithNetboot(d) - require.Equal(t, "Parameter Request List -> Bootfile Name, TFTP Server Name", d.Options[0].String()) + require.Equal(t, "TFTP Server Name, Bootfile Name", GetParameterRequestList(d.Options).String()) } func TestWithRequestedOptions(t *testing.T) { @@ -106,18 +97,16 @@ func TestWithRequestedOptions(t *testing.T) { d, err := New(WithRequestedOptions(OptionFQDN)) require.NoError(t, err) require.NotNil(t, d) - o := d.GetOneOption(OptionParameterRequestList) - require.NotNil(t, o) - opts := o.(*OptParameterRequestList) - require.ElementsMatch(t, opts.RequestedOpts, []OptionCode{OptionFQDN}) + opts := GetParameterRequestList(d.Options) + require.NotNil(t, opts) + require.ElementsMatch(t, opts, []OptionCode{OptionFQDN}) // Check if already set options are preserved WithRequestedOptions(OptionHostName)(d) require.NotNil(t, d) - o = d.GetOneOption(OptionParameterRequestList) - require.NotNil(t, o) - opts = o.(*OptParameterRequestList) - require.ElementsMatch(t, opts.RequestedOpts, []OptionCode{OptionFQDN, OptionHostName}) + opts = GetParameterRequestList(d.Options) + require.NotNil(t, opts) + require.ElementsMatch(t, opts, []OptionCode{OptionFQDN, OptionHostName}) } func TestWithRelay(t *testing.T) { @@ -134,56 +123,44 @@ func TestWithNetmask(t *testing.T) { d, err := New(WithNetmask(net.IPv4Mask(255, 255, 255, 0))) require.NoError(t, err) - require.Equal(t, 1, len(d.Options)) - require.Equal(t, OptionSubnetMask, d.Options[0].Code()) - osm := d.Options[0].(*OptSubnetMask) - require.Equal(t, net.IPv4Mask(255, 255, 255, 0), osm.SubnetMask) + osm := GetSubnetMask(d.Options) + require.Equal(t, net.IPv4Mask(255, 255, 255, 0), osm) } func TestWithLeaseTime(t *testing.T) { d, err := New(WithLeaseTime(uint32(3600))) require.NoError(t, err) - require.Equal(t, 1, len(d.Options)) - require.Equal(t, OptionIPAddressLeaseTime, d.Options[0].Code()) - olt := d.Options[0].(*OptIPAddressLeaseTime) - require.Equal(t, uint32(3600), olt.LeaseTime) + require.True(t, d.Options.Has(OptionIPAddressLeaseTime)) + olt := GetIPAddressLeaseTime(d.Options, 10*time.Second) + require.Equal(t, 3600*time.Second, olt) } func TestWithDNS(t *testing.T) { d, err := New(WithDNS(net.ParseIP("10.0.0.1"), net.ParseIP("10.0.0.2"))) require.NoError(t, err) - require.Equal(t, 1, len(d.Options)) - require.Equal(t, OptionDomainNameServer, d.Options[0].Code()) - olt := d.Options[0].(*OptDomainNameServer) - require.Equal(t, 2, len(olt.NameServers)) - require.Equal(t, net.ParseIP("10.0.0.1"), olt.NameServers[0]) - require.Equal(t, net.ParseIP("10.0.0.2"), olt.NameServers[1]) - require.NotEqual(t, net.ParseIP("10.0.0.1"), olt.NameServers[1]) + dns := GetDNS(d.Options) + require.Equal(t, net.ParseIP("10.0.0.1").To4(), dns[0]) + require.Equal(t, net.ParseIP("10.0.0.2").To4(), dns[1]) } func TestWithDomainSearchList(t *testing.T) { d, err := New(WithDomainSearchList("slackware.it", "dhcp.slackware.it")) require.NoError(t, err) - require.Equal(t, 1, len(d.Options)) - osl := d.Options[0].(*OptDomainSearch) - require.Equal(t, OptionDNSDomainSearchList, osl.Code()) - require.NotNil(t, osl.DomainSearch) - require.Equal(t, 2, len(osl.DomainSearch.Labels)) - require.Equal(t, "slackware.it", osl.DomainSearch.Labels[0]) - require.Equal(t, "dhcp.slackware.it", osl.DomainSearch.Labels[1]) + osl := GetDomainSearch(d.Options) + require.NotNil(t, osl) + require.Equal(t, 2, len(osl.Labels)) + require.Equal(t, "slackware.it", osl.Labels[0]) + require.Equal(t, "dhcp.slackware.it", osl.Labels[1]) } func TestWithRouter(t *testing.T) { - rtr := net.ParseIP("10.0.0.254") + rtr := net.ParseIP("10.0.0.254").To4() d, err := New(WithRouter(rtr)) require.NoError(t, err) - require.Equal(t, 1, len(d.Options)) - ortr := d.Options[0].(*OptRouter) - require.Equal(t, OptionRouter, ortr.Code()) - require.Equal(t, 1, len(ortr.Routers)) - require.Equal(t, rtr, ortr.Routers[0]) + ortr := GetRouter(d.Options) + require.Equal(t, rtr, ortr[0]) } diff --git a/dhcpv4/option_archtype.go b/dhcpv4/option_archtype.go index 59dadb3..00a4417 100644 --- a/dhcpv4/option_archtype.go +++ b/dhcpv4/option_archtype.go @@ -1,55 +1,23 @@ package dhcpv4 import ( - "fmt" - "github.com/insomniacslk/dhcp/iana" - "github.com/u-root/u-root/pkg/uio" ) -// OptClientArchType represents an option encapsulating the Client System -// Architecture Type option definition. See RFC 4578. -type OptClientArchType struct { - ArchTypes []iana.Arch -} - -// Code returns the option code. -func (o *OptClientArchType) Code() OptionCode { - return OptionClientSystemArchitectureType -} - -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptClientArchType) ToBytes() []byte { - buf := uio.NewBigEndianBuffer(nil) - for _, at := range o.ArchTypes { - buf.Write16(uint16(at)) - } - return buf.Data() +// OptClientArch returns a new Client System Architecture Type option. +func OptClientArch(archs ...iana.Arch) Option { + return Option{Code: OptionClientSystemArchitectureType, Value: iana.Archs(archs)} } -// String returns a human-readable string. -func (o *OptClientArchType) String() string { - var archTypes string - for idx, at := range o.ArchTypes { - archTypes += at.String() - if idx < len(o.ArchTypes)-1 { - archTypes += ", " - } +// GetClientArch returns the Client System Architecture Type option. +func GetClientArch(o Options) []iana.Arch { + v := o.Get(OptionClientSystemArchitectureType) + if v == nil { + return nil } - return fmt.Sprintf("Client System Architecture Type -> %v", archTypes) -} - -// ParseOptClientArchType returns a new OptClientArchType from a byte stream, -// or error if any. -func ParseOptClientArchType(data []byte) (*OptClientArchType, error) { - buf := uio.NewBigEndianBuffer(data) - if buf.Len() == 0 { - return nil, fmt.Errorf("must have at least one archtype if option is present") - } - - archTypes := make([]iana.Arch, 0, buf.Len()/2) - for buf.Has(2) { - archTypes = append(archTypes, iana.Arch(buf.Read16())) + var archs iana.Archs + if err := archs.FromBytes(v); err != nil { + return nil } - return &OptClientArchType{ArchTypes: archTypes}, buf.FinError() + return archs } diff --git a/dhcpv4/option_archtype_test.go b/dhcpv4/option_archtype_test.go index 60f8864..fcf526b 100644 --- a/dhcpv4/option_archtype_test.go +++ b/dhcpv4/option_archtype_test.go @@ -8,53 +8,42 @@ import ( ) func TestParseOptClientArchType(t *testing.T) { - data := []byte{ + o := Options{OptionClientSystemArchitectureType.Code(): []byte{ 0, 6, // EFI_IA32 - } - opt, err := ParseOptClientArchType(data) - require.NoError(t, err) - require.Equal(t, opt.ArchTypes[0], iana.EFI_IA32) + }} + archs := GetClientArch(o) + require.NotNil(t, archs) + require.Equal(t, archs[0], iana.EFI_IA32) } func TestParseOptClientArchTypeMultiple(t *testing.T) { - data := []byte{ + o := Options{OptionClientSystemArchitectureType.Code(): []byte{ 0, 6, // EFI_IA32 0, 2, // EFI_ITANIUM - } - opt, err := ParseOptClientArchType(data) - require.NoError(t, err) - require.Equal(t, opt.ArchTypes[0], iana.EFI_IA32) - require.Equal(t, opt.ArchTypes[1], iana.EFI_ITANIUM) + }} + archs := GetClientArch(o) + require.NotNil(t, archs) + require.Equal(t, archs[0], iana.EFI_IA32) + require.Equal(t, archs[1], iana.EFI_ITANIUM) } func TestParseOptClientArchTypeInvalid(t *testing.T) { - data := []byte{42} - _, err := ParseOptClientArchType(data) - require.Error(t, err) + o := Options{OptionClientSystemArchitectureType.Code(): []byte{42}} + archs := GetClientArch(o) + require.Nil(t, archs) } -func TestOptClientArchTypeParseAndToBytes(t *testing.T) { - data := []byte{ - 0, 8, // EFI_XSCALE - } - opt, err := ParseOptClientArchType(data) - require.NoError(t, err) - require.Equal(t, opt.ToBytes(), data) +func TestGetClientArchEmpty(t *testing.T) { + require.Nil(t, GetClientArch(Options{})) } func TestOptClientArchTypeParseAndToBytesMultiple(t *testing.T) { data := []byte{ - 0, 8, // EFI_XSCALE 0, 6, // EFI_IA32 + 0, 8, // EFI_XSCALE } - opt, err := ParseOptClientArchType(data) - require.NoError(t, err) - require.Equal(t, opt.ToBytes(), data) -} - -func TestOptClientArchType(t *testing.T) { - opt := OptClientArchType{ - ArchTypes: []iana.Arch{iana.EFI_ITANIUM}, - } - require.Equal(t, opt.Code(), OptionClientSystemArchitectureType) + opt := OptClientArch(iana.EFI_IA32, iana.EFI_XSCALE) + require.Equal(t, opt.Value.ToBytes(), data) + require.Equal(t, opt.Code, OptionClientSystemArchitectureType) + require.Equal(t, opt.String(), "Client System Architecture Type: EFI IA32, EFI Xscale") } diff --git a/dhcpv4/option_domain_search.go b/dhcpv4/option_domain_search.go index e352e34..6d2f7b2 100644 --- a/dhcpv4/option_domain_search.go +++ b/dhcpv4/option_domain_search.go @@ -1,41 +1,27 @@ package dhcpv4 import ( - "fmt" - "github.com/insomniacslk/dhcp/rfc1035label" ) -// OptDomainSearch implements the domain search list option described by RFC -// 3397, Section 2. +// OptDomainSearch returns a new domain search option. // -// FIXME: rename OptDomainSearch to OptDomainSearchList, and DomainSearch to -// SearchList, for consistency with the equivalent v6 option -type OptDomainSearch struct { - DomainSearch *rfc1035label.Labels -} - -// Code returns the option code. -func (op *OptDomainSearch) Code() OptionCode { - return OptionDNSDomainSearchList -} - -// ToBytes returns a serialized stream of bytes for this option. -func (op *OptDomainSearch) ToBytes() []byte { - return op.DomainSearch.ToBytes() +// The domain search option is described by RFC 3397, Section 2. +func OptDomainSearch(labels *rfc1035label.Labels) Option { + return Option{Code: OptionDNSDomainSearchList, Value: labels} } -// String returns a human-readable string. -func (op *OptDomainSearch) String() string { - return fmt.Sprintf("DNS Domain Search List -> %v", op.DomainSearch.Labels) -} - -// ParseOptDomainSearch returns a new OptDomainSearch from a byte stream, or -// error if any. -func ParseOptDomainSearch(data []byte) (*OptDomainSearch, error) { - labels, err := rfc1035label.FromBytes(data) +// GetDomainSearch returns the domain search list in o, if present. +// +// The domain search option is described by RFC 3397, Section 2. +func GetDomainSearch(o Options) *rfc1035label.Labels { + v := o.Get(OptionDNSDomainSearchList) + if v == nil { + return nil + } + labels, err := rfc1035label.FromBytes(v) if err != nil { - return nil, err + return nil } - return &OptDomainSearch{DomainSearch: labels}, nil + return labels } diff --git a/dhcpv4/option_domain_search_test.go b/dhcpv4/option_domain_search_test.go index 6425d57..9a508f2 100644 --- a/dhcpv4/option_domain_search_test.go +++ b/dhcpv4/option_domain_search_test.go @@ -7,17 +7,20 @@ import ( "github.com/stretchr/testify/require" ) -func TestParseOptDomainSearch(t *testing.T) { +func TestGetDomainSearch(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 := ParseOptDomainSearch(data) - require.NoError(t, err) - require.Equal(t, 2, len(opt.DomainSearch.Labels)) - require.Equal(t, data, opt.DomainSearch.ToBytes()) - require.Equal(t, opt.DomainSearch.Labels[0], "example.com") - require.Equal(t, opt.DomainSearch.Labels[1], "subnet.example.org") + o := Options{ + OptionDNSDomainSearchList.Code(): data, + } + labels := GetDomainSearch(o) + require.NotNil(t, labels) + require.Equal(t, 2, len(labels.Labels)) + require.Equal(t, data, labels.ToBytes()) + require.Equal(t, labels.Labels[0], "example.com") + require.Equal(t, labels.Labels[1], "subnet.example.org") } func TestOptDomainSearchToBytes(t *testing.T) { @@ -25,13 +28,12 @@ func TestOptDomainSearchToBytes(t *testing.T) { 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 := OptDomainSearch{ - DomainSearch: &rfc1035label.Labels{ - Labels: []string{ - "example.com", - "subnet.example.org", - }, + opt := OptDomainSearch(&rfc1035label.Labels{ + Labels: []string{ + "example.com", + "subnet.example.org", }, - } - require.Equal(t, opt.ToBytes(), expected) + }, + ) + require.Equal(t, opt.Value.ToBytes(), expected) } diff --git a/dhcpv4/option_generic.go b/dhcpv4/option_generic.go index 264340c..a54cdeb 100644 --- a/dhcpv4/option_generic.go +++ b/dhcpv4/option_generic.go @@ -1,7 +1,6 @@ package dhcpv4 import ( - "errors" "fmt" ) @@ -9,22 +8,7 @@ import ( // data. Every option that does not have a specific implementation will fall // back to this option. type OptionGeneric struct { - OptionCode OptionCode - Data []byte -} - -// ParseOptionGeneric parses a bytestream and creates a new OptionGeneric from -// it, or an error. -func ParseOptionGeneric(code OptionCode, data []byte) (Option, error) { - if len(data) == 0 { - return nil, errors.New("invalid zero-length bytestream") - } - return &OptionGeneric{OptionCode: code, Data: data}, nil -} - -// Code returns the generic option code. -func (o OptionGeneric) Code() OptionCode { - return o.OptionCode + Data []byte } // ToBytes returns a serialized generic option as a slice of bytes. @@ -34,5 +18,10 @@ func (o OptionGeneric) ToBytes() []byte { // String returns a human-readable representation of a generic option. func (o OptionGeneric) String() string { - return fmt.Sprintf("%v -> %v", o.OptionCode.String(), o.Data) + return fmt.Sprintf("%v", o.Data) +} + +// OptGeneric returns a generic option. +func OptGeneric(code OptionCode, value []byte) Option { + return Option{Code: code, Value: OptionGeneric{value}} } diff --git a/dhcpv4/option_generic_test.go b/dhcpv4/option_generic_test.go index ee35d65..4c4f2e8 100644 --- a/dhcpv4/option_generic_test.go +++ b/dhcpv4/option_generic_test.go @@ -6,42 +6,14 @@ import ( "github.com/stretchr/testify/require" ) -func TestParseOptionGeneric(t *testing.T) { - // Empty bytestream produces error - _, err := ParseOptionGeneric(OptionHostName, []byte{}) - require.Error(t, err, "error from empty bytestream") -} - func TestOptionGenericCode(t *testing.T) { - o := OptionGeneric{ - OptionCode: OptionDHCPMessageType, - Data: []byte{byte(MessageTypeDiscover)}, - } - require.Equal(t, OptionDHCPMessageType, o.Code()) -} - -func TestOptionGenericToBytes(t *testing.T) { - o := OptionGeneric{ - OptionCode: OptionDHCPMessageType, - Data: []byte{byte(MessageTypeDiscover)}, - } - serialized := o.ToBytes() - expected := []byte{1} - require.Equal(t, expected, serialized) -} - -func TestOptionGenericString(t *testing.T) { - o := OptionGeneric{ - OptionCode: OptionDHCPMessageType, - Data: []byte{byte(MessageTypeDiscover)}, - } - require.Equal(t, "DHCP Message Type -> [1]", o.String()) + o := OptGeneric(OptionDHCPMessageType, []byte{byte(MessageTypeDiscover)}) + require.Equal(t, OptionDHCPMessageType, o.Code) + require.Equal(t, []byte{1}, o.Value.ToBytes()) + require.Equal(t, "DHCP Message Type: [1]", o.String()) } func TestOptionGenericStringUnknown(t *testing.T) { - o := OptionGeneric{ - OptionCode: optionCode(102), // Returned option code. - Data: []byte{byte(MessageTypeDiscover)}, - } - require.Equal(t, "unknown (102) -> [1]", o.String()) + o := OptGeneric(optionCode(102), []byte{byte(MessageTypeDiscover)}) + require.Equal(t, "unknown (102): [1]", o.String()) } diff --git a/dhcpv4/option_ip.go b/dhcpv4/option_ip.go index ee0f5fe..6a4206c 100644 --- a/dhcpv4/option_ip.go +++ b/dhcpv4/option_ip.go @@ -1,92 +1,83 @@ package dhcpv4 import ( - "fmt" "net" "github.com/u-root/u-root/pkg/uio" ) -// OptBroadcastAddress implements the broadcast address option described in RFC -// 2132, Section 5.3. -type OptBroadcastAddress struct { - BroadcastAddress net.IP -} +// IP implements DHCPv4 IP option marshaling and unmarshaling as described by +// RFC 2132, Sections 5.3, 9.1, 9.7, and others. +type IP net.IP -// ParseOptBroadcastAddress returns a new OptBroadcastAddress from a byte -// stream, or error if any. -func ParseOptBroadcastAddress(data []byte) (*OptBroadcastAddress, error) { +// FromBytes parses an IP from data in binary form. +func (i *IP) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - return &OptBroadcastAddress{BroadcastAddress: net.IP(buf.CopyN(net.IPv4len))}, buf.FinError() -} - -// Code returns the option code. -func (o *OptBroadcastAddress) Code() OptionCode { - return OptionBroadcastAddress + *i = IP(buf.CopyN(net.IPv4len)) + return buf.FinError() } // ToBytes returns a serialized stream of bytes for this option. -func (o *OptBroadcastAddress) ToBytes() []byte { - return []byte(o.BroadcastAddress.To4()) +func (i IP) ToBytes() []byte { + return []byte(net.IP(i).To4()) } -// String returns a human-readable string. -func (o *OptBroadcastAddress) String() string { - return fmt.Sprintf("Broadcast Address -> %v", o.BroadcastAddress.String()) +// String returns a human-readable IP. +func (i IP) String() string { + return net.IP(i).String() } -// OptRequestedIPAddress implements the requested IP address option described -// by RFC 2132, Section 9.1. -type OptRequestedIPAddress struct { - RequestedAddr net.IP +// GetIP returns code out of o parsed as an IP. +func GetIP(code OptionCode, o Options) net.IP { + v := o.Get(code) + if v == nil { + return nil + } + var ip IP + if err := ip.FromBytes(v); err != nil { + return nil + } + return net.IP(ip) } -// ParseOptRequestedIPAddress returns a new OptRequestedIPAddress from a byte -// stream, or error if any. -func ParseOptRequestedIPAddress(data []byte) (*OptRequestedIPAddress, error) { - buf := uio.NewBigEndianBuffer(data) - return &OptRequestedIPAddress{RequestedAddr: net.IP(buf.CopyN(net.IPv4len))}, buf.FinError() +// GetBroadcastAddress returns the DHCPv4 Broadcast Address value in o. +// +// The broadcast address option is described in RFC 2132, Section 5.3. +func GetBroadcastAddress(o Options) net.IP { + return GetIP(OptionBroadcastAddress, o) } -// Code returns the option code. -func (o *OptRequestedIPAddress) Code() OptionCode { - return OptionRequestedIPAddress -} - -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptRequestedIPAddress) ToBytes() []byte { - return o.RequestedAddr.To4() +// OptBroadcastAddress returns a new DHCPv4 Broadcast Address option. +// +// The broadcast address option is described in RFC 2132, Section 5.3. +func OptBroadcastAddress(ip net.IP) Option { + return Option{Code: OptionBroadcastAddress, Value: IP(ip)} } -// String returns a human-readable string. -func (o *OptRequestedIPAddress) String() string { - return fmt.Sprintf("Requested IP Address -> %v", o.RequestedAddr.String()) +// GetRequestedIPAddress returns the DHCPv4 Requested IP Address value in o. +// +// The requested IP address option is described by RFC 2132, Section 9.1. +func GetRequestedIPAddress(o Options) net.IP { + return GetIP(OptionRequestedIPAddress, o) } -// OptServerIdentifier implements the server identifier option described by RFC -// 2132, Section 9.7. -type OptServerIdentifier struct { - ServerID net.IP +// OptRequestedIPAddress returns a new DHCPv4 Requested IP Address option. +// +// The requested IP address option is described by RFC 2132, Section 9.1. +func OptRequestedIPAddress(ip net.IP) Option { + return Option{Code: OptionRequestedIPAddress, Value: IP(ip)} } -// ParseOptServerIdentifier returns a new OptServerIdentifier from a byte -// stream, or error if any. -func ParseOptServerIdentifier(data []byte) (*OptServerIdentifier, error) { - buf := uio.NewBigEndianBuffer(data) - return &OptServerIdentifier{ServerID: net.IP(buf.CopyN(net.IPv4len))}, buf.FinError() -} - -// Code returns the option code. -func (o *OptServerIdentifier) Code() OptionCode { - return OptionServerIdentifier -} - -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptServerIdentifier) ToBytes() []byte { - return o.ServerID.To4() +// GetServerIdentifier returns the DHCPv4 Server Identifier value in o. +// +// The server identifier option is described by RFC 2132, Section 9.7. +func GetServerIdentifier(o Options) net.IP { + return GetIP(OptionServerIdentifier, o) } -// String returns a human-readable string. -func (o *OptServerIdentifier) String() string { - return fmt.Sprintf("Server Identifier -> %v", o.ServerID.String()) +// OptServerIdentifier returns a new DHCPv4 Server Identifier option. +// +// The server identifier option is described by RFC 2132, Section 9.7. +func OptServerIdentifier(ip net.IP) Option { + return Option{Code: OptionServerIdentifier, Value: IP(ip)} } diff --git a/dhcpv4/option_ip_address_lease_time.go b/dhcpv4/option_ip_address_lease_time.go index 4362419..6e09233 100644 --- a/dhcpv4/option_ip_address_lease_time.go +++ b/dhcpv4/option_ip_address_lease_time.go @@ -2,37 +2,53 @@ package dhcpv4 import ( "fmt" + "time" "github.com/u-root/u-root/pkg/uio" ) -// OptIPAddressLeaseTime implements the IP address lease time option described -// by RFC 2132, Section 9.2. -type OptIPAddressLeaseTime struct { - LeaseTime uint32 -} +// Duration implements the IP address lease time option described by RFC 2132, +// Section 9.2. +type Duration time.Duration -// ParseOptIPAddressLeaseTime constructs an OptIPAddressLeaseTime struct from a -// sequence of bytes and returns it, or an error. -func ParseOptIPAddressLeaseTime(data []byte) (*OptIPAddressLeaseTime, error) { +// FromBytes parses a duration from a byte stream according to RFC 2132, Section 9.2. +func (d *Duration) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - leaseTime := buf.Read32() - return &OptIPAddressLeaseTime{LeaseTime: leaseTime}, buf.FinError() -} - -// Code returns the option code. -func (o *OptIPAddressLeaseTime) Code() OptionCode { - return OptionIPAddressLeaseTime + *d = Duration(time.Duration(buf.Read32()) * time.Second) + return buf.FinError() } // ToBytes returns a serialized stream of bytes for this option. -func (o *OptIPAddressLeaseTime) ToBytes() []byte { +func (d Duration) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) - buf.Write32(o.LeaseTime) + buf.Write32(uint32(time.Duration(d) / time.Second)) return buf.Data() } // String returns a human-readable string for this option. -func (o *OptIPAddressLeaseTime) String() string { - return fmt.Sprintf("IP Addresses Lease Time -> %v", o.LeaseTime) +func (d Duration) String() string { + return fmt.Sprintf("%s", time.Duration(d)) +} + +// OptIPAddressLeaseTime returns a new IP address lease time option. +// +// The IP address lease time option is described by RFC 2132, Section 9.2. +func OptIPAddressLeaseTime(d time.Duration) Option { + return Option{Code: OptionIPAddressLeaseTime, Value: Duration(d)} +} + +// GetIPAddressLeaseTime returns the IP address lease time in o, or the given +// default duration if not present. +// +// The IP address lease time option is described by RFC 2132, Section 9.2. +func GetIPAddressLeaseTime(o Options, def time.Duration) time.Duration { + v := o.Get(OptionIPAddressLeaseTime) + if v == nil { + return def + } + var d Duration + if err := d.FromBytes(v); err != nil { + return def + } + return time.Duration(d) } diff --git a/dhcpv4/option_ip_address_lease_time_test.go b/dhcpv4/option_ip_address_lease_time_test.go index 384db1c..70c4047 100644 --- a/dhcpv4/option_ip_address_lease_time_test.go +++ b/dhcpv4/option_ip_address_lease_time_test.go @@ -2,34 +2,33 @@ package dhcpv4 import ( "testing" + "time" "github.com/stretchr/testify/require" ) -func TestOptIPAddressLeaseTimeInterfaceMethods(t *testing.T) { - o := OptIPAddressLeaseTime{LeaseTime: 43200} - require.Equal(t, OptionIPAddressLeaseTime, o.Code(), "Code") - require.Equal(t, []byte{0, 0, 168, 192}, o.ToBytes(), "ToBytes") +func TestOptIPAddressLeaseTime(t *testing.T) { + o := OptIPAddressLeaseTime(43200 * time.Second) + require.Equal(t, OptionIPAddressLeaseTime, o.Code, "Code") + require.Equal(t, []byte{0, 0, 168, 192}, o.Value.ToBytes(), "ToBytes") + require.Equal(t, "IP Addresses Lease Time: 12h0m0s", o.String(), "String") } -func TestParseOptIPAddressLeaseTime(t *testing.T) { - data := []byte{0, 0, 168, 192} - o, err := ParseOptIPAddressLeaseTime(data) - require.NoError(t, err) - require.Equal(t, &OptIPAddressLeaseTime{LeaseTime: 43200}, o) +func TestGetIPAddressLeaseTime(t *testing.T) { + o := Options{OptionIPAddressLeaseTime.Code(): []byte{0, 0, 168, 192}} + leaseTime := GetIPAddressLeaseTime(o, 0) + require.Equal(t, 43200*time.Second, leaseTime) - // Short byte stream - data = []byte{168, 192} - _, err = ParseOptIPAddressLeaseTime(data) - require.Error(t, err, "should get error from short byte stream") + // Too short. + o = Options{OptionIPAddressLeaseTime.Code(): []byte{168, 192}} + leaseTime = GetIPAddressLeaseTime(o, 0) + require.Equal(t, time.Duration(0), leaseTime) - // Bad length - data = []byte{1, 1, 1, 1, 1} - _, err = ParseOptIPAddressLeaseTime(data) - require.Error(t, err, "should get error from bad length") -} + // Too long. + o = Options{OptionIPAddressLeaseTime.Code(): []byte{1, 1, 1, 1, 1}} + leaseTime = GetIPAddressLeaseTime(o, 0) + require.Equal(t, time.Duration(0), leaseTime) -func TestOptIPAddressLeaseTimeString(t *testing.T) { - o := OptIPAddressLeaseTime{LeaseTime: 43200} - require.Equal(t, "IP Addresses Lease Time -> 43200", o.String()) + // Empty. + require.Equal(t, time.Duration(10), GetIPAddressLeaseTime(Options{}, 10)) } diff --git a/dhcpv4/option_ip_test.go b/dhcpv4/option_ip_test.go index fe31487..e772224 100644 --- a/dhcpv4/option_ip_test.go +++ b/dhcpv4/option_ip_test.go @@ -8,61 +8,59 @@ import ( ) func TestOptBroadcastAddress(t *testing.T) { - o := OptBroadcastAddress{BroadcastAddress: net.IP{192, 168, 0, 1}} + ip := net.IP{192, 168, 0, 1} + o := OptBroadcastAddress(ip) - require.Equal(t, OptionBroadcastAddress, o.Code(), "Code") - require.Equal(t, []byte{192, 168, 0, 1}, o.ToBytes(), "ToBytes") - require.Equal(t, "Broadcast Address -> 192.168.0.1", o.String(), "String") + require.Equal(t, OptionBroadcastAddress, o.Code, "Code") + require.Equal(t, []byte(ip), o.Value.ToBytes(), "ToBytes") + require.Equal(t, "Broadcast Address: 192.168.0.1", o.String(), "String") } -func TestParseOptBroadcastAddress(t *testing.T) { - o, err := ParseOptBroadcastAddress([]byte{}) - require.Error(t, err, "empty byte stream") - - o, err = ParseOptBroadcastAddress([]byte{192, 168, 0}) - require.Error(t, err, "wrong IP length") +func TestGetIPs(t *testing.T) { + o := Options{102: []byte{}} + i := GetIPs(optionCode(102), o) + require.Nil(t, i) - o, err = ParseOptBroadcastAddress([]byte{192, 168, 0, 1}) - require.NoError(t, err) - require.Equal(t, net.IP{192, 168, 0, 1}, o.BroadcastAddress) -} + o = Options{102: []byte{192, 168, 0}} + i = GetIPs(optionCode(102), o) + require.Nil(t, i) -func TestOptRequestedIPAddress(t *testing.T) { - o := OptRequestedIPAddress{RequestedAddr: net.IP{192, 168, 0, 1}} + o = Options{102: []byte{192, 168, 0, 1}} + i = GetIPs(optionCode(102), o) + require.Equal(t, i, []net.IP{{192, 168, 0, 1}}) - require.Equal(t, OptionRequestedIPAddress, o.Code(), "Code") - require.Equal(t, []byte{192, 168, 0, 1}, o.ToBytes(), "ToBytes") - require.Equal(t, "Requested IP Address -> 192.168.0.1", o.String(), "String") + o = Options{102: []byte{192, 168, 0, 1, 192, 168, 0, 2}} + i = GetIPs(optionCode(102), o) + require.Equal(t, i, []net.IP{{192, 168, 0, 1}, {192, 168, 0, 2}}) } -func TestParseOptRequestedIPAddress(t *testing.T) { - o, err := ParseOptRequestedIPAddress([]byte{}) +func TestParseIP(t *testing.T) { + var ip IP + err := ip.FromBytes([]byte{}) require.Error(t, err, "empty byte stream") - o, err = ParseOptRequestedIPAddress([]byte{192}) + err = ip.FromBytes([]byte{192, 168, 0}) require.Error(t, err, "wrong IP length") - o, err = ParseOptRequestedIPAddress([]byte{192, 168, 0, 1}) + err = ip.FromBytes([]byte{192, 168, 0, 1}) require.NoError(t, err) - require.Equal(t, net.IP{192, 168, 0, 1}, o.RequestedAddr) + require.Equal(t, net.IP{192, 168, 0, 1}, net.IP(ip)) } -func TestOptServerIdentifierInterfaceMethods(t *testing.T) { - o := OptServerIdentifier{ServerID: net.IP{192, 168, 0, 1}} +func TestOptRequestedIPAddress(t *testing.T) { + ip := net.IP{192, 168, 0, 1} + o := OptRequestedIPAddress(ip) - require.Equal(t, OptionServerIdentifier, o.Code(), "Code") - require.Equal(t, []byte{192, 168, 0, 1}, o.ToBytes(), "ToBytes") - require.Equal(t, "Server Identifier -> 192.168.0.1", o.String(), "String") + require.Equal(t, OptionRequestedIPAddress, o.Code, "Code") + require.Equal(t, []byte(ip), o.Value.ToBytes(), "ToBytes") + require.Equal(t, "Requested IP Address: 192.168.0.1", o.String(), "String") } -func TestParseOptServerIdentifier(t *testing.T) { - o, err := ParseOptServerIdentifier([]byte{}) - require.Error(t, err, "empty byte stream") +func TestOptServerIdentifier(t *testing.T) { + ip := net.IP{192, 168, 0, 1} + o := OptServerIdentifier(ip) - o, err = ParseOptServerIdentifier([]byte{192, 168, 0}) - require.Error(t, err, "wrong IP length") - - o, err = ParseOptServerIdentifier([]byte{192, 168, 0, 1}) - require.NoError(t, err) - require.Equal(t, net.IP{192, 168, 0, 1}, o.ServerID) + require.Equal(t, OptionServerIdentifier, o.Code, "Code") + require.Equal(t, []byte(ip), o.Value.ToBytes(), "ToBytes") + require.Equal(t, "Server Identifier: 192.168.0.1", o.String(), "String") } diff --git a/dhcpv4/option_ips.go b/dhcpv4/option_ips.go index 8792ed0..693d62d 100644 --- a/dhcpv4/option_ips.go +++ b/dhcpv4/option_ips.go @@ -8,36 +8,40 @@ import ( "github.com/u-root/u-root/pkg/uio" ) -// ParseIPs parses an IPv4 address from a DHCP packet as used and specified by +// IPs are IPv4 addresses from a DHCP packet as used and specified by options +// in RFC 2132, Sections 3.5 through 3.13, 8.2, 8.3, 8.5, 8.6, 8.9, and 8.10. +// +// IPs implements the OptionValue type. +type IPs []net.IP + +// FromBytes parses an IPv4 address from a DHCP packet as used and specified by // options in RFC 2132, Sections 3.5 through 3.13, 8.2, 8.3, 8.5, 8.6, 8.9, and // 8.10. -func ParseIPs(data []byte) ([]net.IP, error) { +func (i *IPs) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - if buf.Len() == 0 { - return nil, fmt.Errorf("IP DHCP options must always list at least one IP") + return fmt.Errorf("IP DHCP options must always list at least one IP") } - ips := make([]net.IP, 0, buf.Len()/net.IPv4len) + *i = make(IPs, 0, buf.Len()/net.IPv4len) for buf.Has(net.IPv4len) { - ips = append(ips, net.IP(buf.CopyN(net.IPv4len))) + *i = append(*i, net.IP(buf.CopyN(net.IPv4len))) } - return ips, buf.FinError() + return buf.FinError() } -// IPsToBytes marshals an IPv4 address to a DHCP packet as specified by RFC -// 2132, Section 3.5 et al. -func IPsToBytes(i []net.IP) []byte { +// ToBytes marshals IPv4 addresses to a DHCP packet as specified by RFC 2132, +// Section 3.5 et al. +func (i IPs) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) - for _, ip := range i { buf.WriteBytes(ip.To4()) } return buf.Data() } -// IPsToString returns a human-readable representation of a list of IPs. -func IPsToString(i []net.IP) string { +// String returns a human-readable representation of a list of IPs. +func (i IPs) String() string { s := make([]string, 0, len(i)) for _, ip := range i { s = append(s, ip.String()) @@ -45,92 +49,76 @@ func IPsToString(i []net.IP) string { return strings.Join(s, ", ") } -// OptRouter implements the router option described by RFC 2132, Section 3.5. -type OptRouter struct { - Routers []net.IP -} - -// ParseOptRouter returns a new OptRouter from a byte stream, or error if any. -func ParseOptRouter(data []byte) (*OptRouter, error) { - ips, err := ParseIPs(data) - if err != nil { - return nil, err +// GetIPs parses a list of IPs from code in o. +func GetIPs(code OptionCode, o Options) []net.IP { + v := o.Get(code) + if v == nil { + return nil } - return &OptRouter{Routers: ips}, nil -} - -// Code returns the option code. -func (o *OptRouter) Code() OptionCode { - return OptionRouter -} - -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptRouter) ToBytes() []byte { - return IPsToBytes(o.Routers) -} - -// String returns a human-readable string. -func (o *OptRouter) String() string { - return fmt.Sprintf("Routers -> %s", IPsToString(o.Routers)) -} - -// OptNTPServers implements the NTP servers option described by RFC 2132, -// Section 8.3. -type OptNTPServers struct { - NTPServers []net.IP -} - -// ParseOptNTPServers returns a new OptNTPServers from a byte stream, or error if any. -func ParseOptNTPServers(data []byte) (*OptNTPServers, error) { - ips, err := ParseIPs(data) - if err != nil { - return nil, err + var ips IPs + if err := ips.FromBytes(v); err != nil { + return nil } - return &OptNTPServers{NTPServers: ips}, nil + return []net.IP(ips) } -// Code returns the option code. -func (o *OptNTPServers) Code() OptionCode { - return OptionNTPServers +// GetRouter parses the DHCPv4 Router option if present. +// +// The Router option is described by RFC 2132, Section 3.5. +func GetRouter(o Options) []net.IP { + return GetIPs(OptionRouter, o) } -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptNTPServers) ToBytes() []byte { - return IPsToBytes(o.NTPServers) +// OptRouter returns a new DHCPv4 Router option. +// +// The Router option is described by RFC 2132, Section 3.5. +func OptRouter(routers ...net.IP) Option { + return Option{ + Code: OptionRouter, + Value: IPs(routers), + } } -// String returns a human-readable string. -func (o *OptNTPServers) String() string { - return fmt.Sprintf("NTP Servers -> %v", IPsToString(o.NTPServers)) +// WithRouter updates a packet with the DHCPv4 Router option. +func WithRouter(routers ...net.IP) Modifier { + return WithOption(OptRouter(routers...)) } -// OptDomainNameServer implements the DNS server option described by RFC 2132, -// Section 3.8. -type OptDomainNameServer struct { - NameServers []net.IP +// GetNTPServers parses the DHCPv4 NTP Servers option if present. +// +// The NTP servers option is described by RFC 2132, Section 8.3. +func GetNTPServers(o Options) []net.IP { + return GetIPs(OptionNTPServers, o) } -// ParseOptDomainNameServer returns a new OptDomainNameServer from a byte -// stream, or error if any. -func ParseOptDomainNameServer(data []byte) (*OptDomainNameServer, error) { - ips, err := ParseIPs(data) - if err != nil { - return nil, err +// OptNTPServers returns a new DHCPv4 NTP Server option. +// +// The NTP servers option is described by RFC 2132, Section 8.3. +func OptNTPServers(ntpServers ...net.IP) Option { + return Option{ + Code: OptionNTPServers, + Value: IPs(ntpServers), } - return &OptDomainNameServer{NameServers: ips}, nil } -// Code returns the option code. -func (o *OptDomainNameServer) Code() OptionCode { - return OptionDomainNameServer +// GetDNS parses the DHCPv4 Domain Name Server option if present. +// +// The DNS server option is described by RFC 2132, Section 3.8. +func GetDNS(o Options) []net.IP { + return GetIPs(OptionDomainNameServer, o) } -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptDomainNameServer) ToBytes() []byte { - return IPsToBytes(o.NameServers) +// OptDNS returns a new DHCPv4 Domain Name Server option. +// +// The DNS server option is described by RFC 2132, Section 3.8. +func OptDNS(servers ...net.IP) Option { + return Option{ + Code: OptionDomainNameServer, + Value: IPs(servers), + } } -// String returns a human-readable string. -func (o *OptDomainNameServer) String() string { - return fmt.Sprintf("Domain Name Servers -> %s", IPsToString(o.NameServers)) +// WithDNS modifies a packet with the DHCPv4 Domain Name Server option. +func WithDNS(servers ...net.IP) Modifier { + return WithOption(OptDNS(servers...)) } diff --git a/dhcpv4/option_ips_test.go b/dhcpv4/option_ips_test.go index 5efd537..1e1b772 100644 --- a/dhcpv4/option_ips_test.go +++ b/dhcpv4/option_ips_test.go @@ -7,132 +7,77 @@ import ( "github.com/stretchr/testify/require" ) -func TestOptRoutersInterfaceMethods(t *testing.T) { - routers := []net.IP{ - net.IPv4(192, 168, 0, 10), - net.IPv4(192, 168, 0, 20), - } - o := OptRouter{Routers: routers} - require.Equal(t, OptionRouter, o.Code(), "Code") - require.Equal(t, routers, o.Routers, "Routers") -} - -func TestParseOptRouter(t *testing.T) { - data := []byte{ - byte(OptionRouter), - 8, // Length - 192, 168, 0, 10, // Router #1 - 192, 168, 0, 20, // Router #2 - } - o, err := ParseOptRouter(data[2:]) - require.NoError(t, err) - routers := []net.IP{ - net.IP{192, 168, 0, 10}, - net.IP{192, 168, 0, 20}, - } - require.Equal(t, &OptRouter{Routers: routers}, o) - - // Short byte stream - data = []byte{byte(OptionRouter)} - _, err = ParseOptRouter(data) - require.Error(t, err, "should get error from short byte stream") -} - -func TestParseOptRouterNoRouters(t *testing.T) { - // RFC2132 requires that at least one Router IP is specified - data := []byte{ - byte(OptionRouter), - 0, // Length - } - _, err := ParseOptRouter(data) - require.Error(t, err) -} - -func TestOptRouterString(t *testing.T) { - o := OptRouter{Routers: []net.IP{net.IP{192, 168, 0, 1}, net.IP{192, 168, 0, 10}}} - require.Equal(t, "Routers -> 192.168.0.1, 192.168.0.10", o.String()) -} - -func TestOptDomainNameServerInterfaceMethods(t *testing.T) { - servers := []net.IP{ - net.IPv4(192, 168, 0, 10), - net.IPv4(192, 168, 0, 20), - } - o := OptDomainNameServer{NameServers: servers} - require.Equal(t, OptionDomainNameServer, o.Code(), "Code") - require.Equal(t, servers, o.NameServers, "NameServers") -} - -func TestParseOptDomainNameServer(t *testing.T) { +func TestParseIPs(t *testing.T) { + var i IPs data := []byte{ - byte(OptionDomainNameServer), - 8, // Length 192, 168, 0, 10, // DNS #1 192, 168, 0, 20, // DNS #2 } - o, err := ParseOptDomainNameServer(data[2:]) + err := i.FromBytes(data) require.NoError(t, err) servers := []net.IP{ net.IP{192, 168, 0, 10}, net.IP{192, 168, 0, 20}, } - require.Equal(t, &OptDomainNameServer{NameServers: servers}, o) + require.Equal(t, servers, []net.IP(i)) // Bad length data = []byte{1, 1, 1} - _, err = ParseOptDomainNameServer(data) + err = i.FromBytes(data) require.Error(t, err, "should get error from bad length") -} -func TestParseOptDomainNameServerNoServers(t *testing.T) { - // RFC2132 requires that at least one DNS server IP is specified - _, err := ParseOptDomainNameServer([]byte{}) + // RFC2132 requires that at least one IP is specified for each IP field. + err = i.FromBytes([]byte{}) require.Error(t, err) } -func TestOptDomainNameServerString(t *testing.T) { - o := OptDomainNameServer{NameServers: []net.IP{net.IPv4(192, 168, 0, 1), net.IPv4(192, 168, 0, 10)}} - require.Equal(t, "Domain Name Servers -> 192.168.0.1, 192.168.0.10", o.String()) +func TestOptDomainNameServer(t *testing.T) { + o := OptDNS(net.IPv4(192, 168, 0, 1), net.IPv4(192, 168, 0, 10)) + require.Equal(t, OptionDomainNameServer, o.Code) + require.Equal(t, []byte{192, 168, 0, 1, 192, 168, 0, 10}, o.Value.ToBytes()) + require.Equal(t, "Domain Name Server: 192.168.0.1, 192.168.0.10", o.String()) } -func TestOptNTPServersInterfaceMethods(t *testing.T) { - ntpServers := []net.IP{ - net.IPv4(192, 168, 0, 10), - net.IPv4(192, 168, 0, 20), +func TestGetDomainNameServer(t *testing.T) { + ips := []net.IP{ + net.IP{192, 168, 0, 1}, + net.IP{192, 168, 0, 10}, } - o := OptNTPServers{NTPServers: ntpServers} - require.Equal(t, OptionNTPServers, o.Code(), "Code") - require.Equal(t, ntpServers, o.NTPServers, "NTPServers") + o := OptionsFromList(OptDNS(ips...)) + require.Equal(t, ips, GetDNS(o)) + require.Nil(t, GetDNS(Options{})) } -func TestParseOptNTPServers(t *testing.T) { - data := []byte{ - byte(OptionNTPServers), - 8, // Length - 192, 168, 0, 10, // NTP server #1 - 192, 168, 0, 20, // NTP server #2 - } - o, err := ParseOptNTPServers(data[2:]) - require.NoError(t, err) - ntpServers := []net.IP{ +func TestOptNTPServers(t *testing.T) { + o := OptNTPServers(net.IPv4(192, 168, 0, 1), net.IPv4(192, 168, 0, 10)) + require.Equal(t, OptionNTPServers, o.Code) + require.Equal(t, []byte{192, 168, 0, 1, 192, 168, 0, 10}, o.Value.ToBytes()) + require.Equal(t, "NTP Servers: 192.168.0.1, 192.168.0.10", o.String()) +} + +func TestGetNTPServers(t *testing.T) { + ips := []net.IP{ + net.IP{192, 168, 0, 1}, net.IP{192, 168, 0, 10}, - net.IP{192, 168, 0, 20}, } - require.Equal(t, &OptNTPServers{NTPServers: ntpServers}, o) - - // Bad length - data = []byte{1, 1, 1} - _, err = ParseOptNTPServers(data) - require.Error(t, err, "should get error from bad length") + o := OptionsFromList(OptNTPServers(ips...)) + require.Equal(t, ips, GetNTPServers(o)) + require.Nil(t, GetNTPServers(Options{})) } -func TestParseOptNTPserversNoNTPServers(t *testing.T) { - // RFC2132 requires that at least one NTP server IP is specified - _, err := ParseOptNTPServers([]byte{}) - require.Error(t, err) +func TestOptRouter(t *testing.T) { + o := OptRouter(net.IPv4(192, 168, 0, 1), net.IPv4(192, 168, 0, 10)) + require.Equal(t, OptionRouter, o.Code) + require.Equal(t, []byte{192, 168, 0, 1, 192, 168, 0, 10}, o.Value.ToBytes()) + require.Equal(t, "Router: 192.168.0.1, 192.168.0.10", o.String()) } -func TestOptNTPServersString(t *testing.T) { - o := OptNTPServers{NTPServers: []net.IP{net.IPv4(192, 168, 0, 1), net.IPv4(192, 168, 0, 10)}} - require.Equal(t, "NTP Servers -> 192.168.0.1, 192.168.0.10", o.String()) +func TestGetRouter(t *testing.T) { + ips := []net.IP{ + net.IP{192, 168, 0, 1}, + net.IP{192, 168, 0, 10}, + } + o := OptionsFromList(OptRouter(ips...)) + require.Equal(t, ips, GetRouter(o)) + require.Nil(t, GetRouter(Options{})) } diff --git a/dhcpv4/option_maximum_dhcp_message_size.go b/dhcpv4/option_maximum_dhcp_message_size.go index 904f3d2..900eea5 100644 --- a/dhcpv4/option_maximum_dhcp_message_size.go +++ b/dhcpv4/option_maximum_dhcp_message_size.go @@ -6,32 +6,52 @@ import ( "github.com/u-root/u-root/pkg/uio" ) -// OptMaximumDHCPMessageSize implements the maximum DHCP message size option -// described by RFC 2132, Section 9.10. -type OptMaximumDHCPMessageSize struct { - Size uint16 +// Uint16 implements encoding and decoding functions for a uint16 as used in +// RFC 2132, Section 9.10. +type Uint16 uint16 + +// ToBytes returns a serialized stream of bytes for this option. +func (o Uint16) ToBytes() []byte { + buf := uio.NewBigEndianBuffer(nil) + buf.Write16(uint16(o)) + return buf.Data() } -// ParseOptMaximumDHCPMessageSize constructs an OptMaximumDHCPMessageSize -// struct from a sequence of bytes and returns it, or an error. -func ParseOptMaximumDHCPMessageSize(data []byte) (*OptMaximumDHCPMessageSize, error) { +// String returns a human-readable string for this option. +func (o Uint16) String() string { + return fmt.Sprintf("%d", uint16(o)) +} + +// FromBytes decodes data into o as per RFC 2132, Section 9.10. +func (o *Uint16) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - return &OptMaximumDHCPMessageSize{Size: buf.Read16()}, buf.FinError() + *o = Uint16(buf.Read16()) + return buf.FinError() } -// Code returns the option code. -func (o *OptMaximumDHCPMessageSize) Code() OptionCode { - return OptionMaximumDHCPMessageSize +// GetUint16 parses a uint16 from code in o. +func GetUint16(code OptionCode, o Options) (uint16, error) { + v := o.Get(code) + if v == nil { + return 0, fmt.Errorf("option not present") + } + var u Uint16 + if err := u.FromBytes(v); err != nil { + return 0, err + } + return uint16(u), nil } -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptMaximumDHCPMessageSize) ToBytes() []byte { - buf := uio.NewBigEndianBuffer(nil) - buf.Write16(o.Size) - return buf.Data() +// OptMaxMessageSize returns a new DHCP Maximum Message Size option. +// +// The Maximum DHCP Message Size option is described by RFC 2132, Section 9.10. +func OptMaxMessageSize(size uint16) Option { + return Option{Code: OptionMaximumDHCPMessageSize, Value: Uint16(size)} } -// String returns a human-readable string for this option. -func (o *OptMaximumDHCPMessageSize) String() string { - return fmt.Sprintf("Maximum DHCP Message Size -> %v", o.Size) +// GetMaxMessageSize returns the DHCP Maximum Message Size in o if present. +// +// The Maximum DHCP Message Size option is described by RFC 2132, Section 9.10. +func GetMaxMessageSize(o Options) (uint16, error) { + return GetUint16(OptionMaximumDHCPMessageSize, o) } diff --git a/dhcpv4/option_maximum_dhcp_message_size_test.go b/dhcpv4/option_maximum_dhcp_message_size_test.go index 0b36b0e..147f280 100644 --- a/dhcpv4/option_maximum_dhcp_message_size_test.go +++ b/dhcpv4/option_maximum_dhcp_message_size_test.go @@ -6,30 +6,26 @@ import ( "github.com/stretchr/testify/require" ) -func TestOptMaximumDHCPMessageSizeInterfaceMethods(t *testing.T) { - o := OptMaximumDHCPMessageSize{Size: 1500} - require.Equal(t, OptionMaximumDHCPMessageSize, o.Code(), "Code") - require.Equal(t, []byte{5, 220}, o.ToBytes(), "ToBytes") +func TestOptMaximumDHCPMessageSize(t *testing.T) { + o := OptMaxMessageSize(1500) + require.Equal(t, OptionMaximumDHCPMessageSize, o.Code, "Code") + require.Equal(t, []byte{5, 220}, o.Value.ToBytes(), "ToBytes") + require.Equal(t, "Maximum DHCP Message Size: 1500", o.String()) } -func TestParseOptMaximumDHCPMessageSize(t *testing.T) { - data := []byte{5, 220} - o, err := ParseOptMaximumDHCPMessageSize(data) +func TestGetMaximumDHCPMessageSize(t *testing.T) { + options := Options{OptionMaximumDHCPMessageSize.Code(): []byte{5, 220}} + o, err := GetMaxMessageSize(options) require.NoError(t, err) - require.Equal(t, &OptMaximumDHCPMessageSize{Size: 1500}, o) + require.Equal(t, uint16(1500), o) // Short byte stream - data = []byte{2} - _, err = ParseOptMaximumDHCPMessageSize(data) + options = Options{OptionMaximumDHCPMessageSize.Code(): []byte{2}} + _, err = GetMaxMessageSize(options) require.Error(t, err, "should get error from short byte stream") // Bad length - data = []byte{1, 1, 1} - _, err = ParseOptMaximumDHCPMessageSize(data) + options = Options{OptionMaximumDHCPMessageSize.Code(): []byte{1, 1, 1}} + _, err = GetMaxMessageSize(options) require.Error(t, err, "should get error from bad length") } - -func TestOptMaximumDHCPMessageSizeString(t *testing.T) { - o := OptMaximumDHCPMessageSize{Size: 1500} - require.Equal(t, "Maximum DHCP Message Size -> 1500", o.String()) -} diff --git a/dhcpv4/option_message_type.go b/dhcpv4/option_message_type.go index 2857e41..141e3b7 100644 --- a/dhcpv4/option_message_type.go +++ b/dhcpv4/option_message_type.go @@ -1,35 +1,19 @@ package dhcpv4 -import ( - "fmt" - - "github.com/u-root/u-root/pkg/uio" -) - -// OptMessageType implements the DHCP message type option described by RFC -// 2132, Section 9.6. -type OptMessageType struct { - MessageType MessageType -} - -// ParseOptMessageType constructs an OptMessageType struct from a sequence of -// bytes and returns it, or an error. -func ParseOptMessageType(data []byte) (*OptMessageType, error) { - buf := uio.NewBigEndianBuffer(data) - return &OptMessageType{MessageType: MessageType(buf.Read8())}, buf.FinError() -} - -// Code returns the option code. -func (o *OptMessageType) Code() OptionCode { - return OptionDHCPMessageType -} - -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptMessageType) ToBytes() []byte { - return []byte{byte(o.MessageType)} +// OptMessageType returns a new DHCPv4 Message Type option. +func OptMessageType(m MessageType) Option { + return Option{Code: OptionDHCPMessageType, Value: m} } -// String returns a human-readable string for this option. -func (o *OptMessageType) String() string { - return fmt.Sprintf("DHCP Message Type -> %s", o.MessageType.String()) +// GetMessageType returns the DHCPv4 Message Type option in o. +func GetMessageType(o Options) MessageType { + v := o.Get(OptionDHCPMessageType) + if v == nil { + return MessageTypeNone + } + var m MessageType + if err := m.FromBytes(v); err != nil { + return MessageTypeNone + } + return m } diff --git a/dhcpv4/option_message_type_test.go b/dhcpv4/option_message_type_test.go index c3b4904..a97889e 100644 --- a/dhcpv4/option_message_type_test.go +++ b/dhcpv4/option_message_type_test.go @@ -6,36 +6,32 @@ import ( "github.com/stretchr/testify/require" ) -func TestOptMessageTypeInterfaceMethods(t *testing.T) { - o := OptMessageType{MessageType: MessageTypeDiscover} - require.Equal(t, OptionDHCPMessageType, o.Code(), "Code") - require.Equal(t, []byte{1}, o.ToBytes(), "ToBytes") -} +func TestOptMessageType(t *testing.T) { + o := OptMessageType(MessageTypeDiscover) + require.Equal(t, OptionDHCPMessageType, o.Code, "Code") + require.Equal(t, []byte{1}, o.Value.ToBytes(), "ToBytes") + require.Equal(t, "DHCP Message Type: DISCOVER", o.String()) -func TestOptMessageTypeNew(t *testing.T) { - o := OptMessageType{MessageType: MessageTypeDiscover} - require.Equal(t, OptionDHCPMessageType, o.Code()) - require.Equal(t, MessageTypeDiscover, o.MessageType) + // unknown + o = OptMessageType(99) + require.Equal(t, "DHCP Message Type: unknown (99)", o.String()) } func TestParseOptMessageType(t *testing.T) { + var m MessageType data := []byte{1} // DISCOVER - o, err := ParseOptMessageType(data) + err := m.FromBytes(data) require.NoError(t, err) - require.Equal(t, &OptMessageType{MessageType: MessageTypeDiscover}, o) + require.Equal(t, MessageTypeDiscover, m) // Bad length data = []byte{1, 2} - _, err = ParseOptMessageType(data) + err = m.FromBytes(data) require.Error(t, err, "should get error from bad length") } -func TestOptMessageTypeString(t *testing.T) { - // known - o := OptMessageType{MessageType: MessageTypeDiscover} - require.Equal(t, "DHCP Message Type -> DISCOVER", o.String()) - - // unknown - o = OptMessageType{MessageType: 99} - require.Equal(t, "DHCP Message Type -> unknown (99)", o.String()) +func TestGetMessageType(t *testing.T) { + o := OptionsFromList(OptMessageType(MessageTypeDiscover)) + require.Equal(t, MessageTypeDiscover, GetMessageType(o)) + require.Equal(t, MessageTypeNone, GetMessageType(Options{})) } diff --git a/dhcpv4/option_parameter_request_list.go b/dhcpv4/option_parameter_request_list.go index 750c957..81d5efb 100644 --- a/dhcpv4/option_parameter_request_list.go +++ b/dhcpv4/option_parameter_request_list.go @@ -1,7 +1,7 @@ package dhcpv4 import ( - "fmt" + "sort" "strings" "github.com/u-root/u-root/pkg/uio" @@ -29,47 +29,59 @@ func (ol *OptionCodeList) Add(cs ...OptionCode) { } } +func (ol OptionCodeList) sort() { + sort.Slice(ol, func(i, j int) bool { return ol[i].Code() < ol[j].Code() }) +} + // String returns a human-readable string for the option names. func (ol OptionCodeList) String() string { var names []string + ol.sort() for _, code := range ol { names = append(names, code.String()) } return strings.Join(names, ", ") } -// OptParameterRequestList implements the parameter request list option -// described by RFC 2132, Section 9.8. -type OptParameterRequestList struct { - RequestedOpts OptionCodeList +// ToBytes returns a serialized stream of bytes for this option as defined by +// RFC 2132, Section 9.8. +func (ol OptionCodeList) ToBytes() []byte { + buf := uio.NewBigEndianBuffer(nil) + for _, req := range ol { + buf.Write8(req.Code()) + } + return buf.Data() } -// ParseOptParameterRequestList returns a new OptParameterRequestList from a -// byte stream, or error if any. -func ParseOptParameterRequestList(data []byte) (*OptParameterRequestList, error) { +// FromBytes parses a byte stream for this option as described by RFC 2132, +// Section 9.8. +func (ol *OptionCodeList) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - requestedOpts := make(OptionCodeList, 0, buf.Len()) + *ol = make(OptionCodeList, 0, buf.Len()) for buf.Has(1) { - requestedOpts = append(requestedOpts, optionCode(buf.Read8())) + *ol = append(*ol, optionCode(buf.Read8())) } - return &OptParameterRequestList{RequestedOpts: requestedOpts}, buf.Error() + return buf.FinError() } -// Code returns the option code. -func (o *OptParameterRequestList) Code() OptionCode { - return OptionParameterRequestList -} - -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptParameterRequestList) ToBytes() []byte { - buf := uio.NewBigEndianBuffer(nil) - for _, req := range o.RequestedOpts { - buf.Write8(req.Code()) +// GetParameterRequestList returns the DHCPv4 Parameter Request List in o. +// +// The parameter request list option is described by RFC 2132, Section 9.8. +func GetParameterRequestList(o Options) OptionCodeList { + v := o.Get(OptionParameterRequestList) + if v == nil { + return nil } - return buf.Data() + var codes OptionCodeList + if err := codes.FromBytes(v); err != nil { + return nil + } + return codes } -// String returns a human-readable string for this option. -func (o *OptParameterRequestList) String() string { - return fmt.Sprintf("Parameter Request List -> %s", o.RequestedOpts) +// OptParameterRequestList returns a new DHCPv4 Parameter Request List. +// +// The parameter request list option is described by RFC 2132, Section 9.8. +func OptParameterRequestList(codes ...OptionCode) Option { + return Option{Code: OptionParameterRequestList, Value: OptionCodeList(codes)} } diff --git a/dhcpv4/option_parameter_request_list_test.go b/dhcpv4/option_parameter_request_list_test.go index a09aaad..7c358e2 100644 --- a/dhcpv4/option_parameter_request_list_test.go +++ b/dhcpv4/option_parameter_request_list_test.go @@ -7,24 +7,22 @@ import ( ) func TestOptParameterRequestListInterfaceMethods(t *testing.T) { - requestedOpts := []OptionCode{OptionBootfileName, OptionNameServer} - o := &OptParameterRequestList{RequestedOpts: requestedOpts} - require.Equal(t, OptionParameterRequestList, o.Code(), "Code") + opts := []OptionCode{OptionBootfileName, OptionNameServer} + o := OptParameterRequestList(opts...) + + require.Equal(t, OptionParameterRequestList, o.Code, "Code") expectedBytes := []byte{67, 5} - require.Equal(t, expectedBytes, o.ToBytes(), "ToBytes") + require.Equal(t, expectedBytes, o.Value.ToBytes(), "ToBytes") - expectedString := "Parameter Request List -> Bootfile Name, Name Server" + expectedString := "Parameter Request List: Name Server, Bootfile Name" require.Equal(t, expectedString, o.String(), "String") } func TestParseOptParameterRequestList(t *testing.T) { - var ( - o *OptParameterRequestList - err error - ) - o, err = ParseOptParameterRequestList([]byte{67, 5}) + var o OptionCodeList + err := o.FromBytes([]byte{67, 5}) require.NoError(t, err) expectedOpts := OptionCodeList{OptionBootfileName, OptionNameServer} - require.Equal(t, expectedOpts, o.RequestedOpts) + require.Equal(t, expectedOpts, o) } diff --git a/dhcpv4/option_relay_agent_information.go b/dhcpv4/option_relay_agent_information.go index 42625ca..fb86c70 100644 --- a/dhcpv4/option_relay_agent_information.go +++ b/dhcpv4/option_relay_agent_information.go @@ -2,37 +2,52 @@ package dhcpv4 import ( "fmt" - - "github.com/u-root/u-root/pkg/uio" ) -// OptRelayAgentInformation implements the relay agent info option described by -// RFC 3046. -type OptRelayAgentInformation struct { - Options Options +// RelayOptions is like Options, but stringifies using the Relay Agent Specific +// option space. +type RelayOptions struct { + Options } -// ParseOptRelayAgentInformation returns a new OptRelayAgentInformation from a -// byte stream, or error if any. -func ParseOptRelayAgentInformation(data []byte) (*OptRelayAgentInformation, error) { - options, err := OptionsFromBytesWithParser(data, codeGetter, ParseOptionGeneric, false /* don't check for OptionEnd tag */) - if err != nil { - return nil, err - } - return &OptRelayAgentInformation{Options: options}, nil +var relayHumanizer = OptionHumanizer{ + ValueHumanizer: func(code OptionCode, data []byte) fmt.Stringer { + return OptionGeneric{data} + }, + CodeHumanizer: func(c uint8) OptionCode { + return GenericOptionCode(c) + }, +} + +// String prints the contained options using Relay Agent-specific option code parsing. +func (r RelayOptions) String() string { + return r.Options.ToString(relayHumanizer) } -// Code returns the option code. -func (o *OptRelayAgentInformation) Code() OptionCode { - return OptionRelayAgentInformation +// FromBytes parses relay agent options from data. +func (r *RelayOptions) FromBytes(data []byte) error { + r.Options = make(Options) + return r.Options.FromBytes(data) } -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptRelayAgentInformation) ToBytes() []byte { - return uio.ToBigEndian(o.Options) +// OptRelayAgentInfo returns a new DHCP Relay Agent Info option. +// +// The relay agent info option is described by RFC 3046. +func OptRelayAgentInfo(o ...Option) Option { + return Option{Code: OptionRelayAgentInformation, Value: RelayOptions{OptionsFromList(o...)}} } -// String returns a human-readable string for this option. -func (o *OptRelayAgentInformation) String() string { - return fmt.Sprintf("Relay Agent Information -> %v", o.Options) +// GetRelayAgentInfo returns options embedded by the relay agent. +// +// The relay agent info option is described by RFC 3046. +func GetRelayAgentInfo(o Options) *RelayOptions { + v := o.Get(OptionRelayAgentInformation) + if v == nil { + return nil + } + var relayOptions RelayOptions + if err := relayOptions.FromBytes(v); err != nil { + return nil + } + return &relayOptions } diff --git a/dhcpv4/option_relay_agent_information_test.go b/dhcpv4/option_relay_agent_information_test.go index bb5fae0..6a7b275 100644 --- a/dhcpv4/option_relay_agent_information_test.go +++ b/dhcpv4/option_relay_agent_information_test.go @@ -6,47 +6,46 @@ import ( "github.com/stretchr/testify/require" ) -func TestParseOptRelayAgentInformation(t *testing.T) { - data := []byte{ - 1, 5, 'l', 'i', 'n', 'u', 'x', - 2, 4, 'b', 'o', 'o', 't', +func TestGetRelayAgentInformation(t *testing.T) { + o := Options{ + OptionRelayAgentInformation.Code(): []byte{ + 1, 5, 'l', 'i', 'n', 'u', 'x', + 2, 4, 'b', 'o', 'o', 't', + }, } - // short sub-option bytes - opt, err := ParseOptRelayAgentInformation([]byte{1, 0, 1}) - require.Error(t, err) + opt := GetRelayAgentInfo(o) + require.NotNil(t, opt) + require.Equal(t, len(opt.Options), 2) - // short sub-option length - opt, err = ParseOptRelayAgentInformation([]byte{1, 1}) - require.Error(t, err) + circuit := opt.Get(GenericOptionCode(1)) + remote := opt.Get(GenericOptionCode(2)) + require.Equal(t, circuit, []byte("linux")) + require.Equal(t, remote, []byte("boot")) - opt, err = ParseOptRelayAgentInformation(data) - require.NoError(t, err) - require.Equal(t, len(opt.Options), 2) - circuit := opt.Options.GetOne(optionCode(1)).(*OptionGeneric) - require.NoError(t, err) - remote := opt.Options.GetOne(optionCode(2)).(*OptionGeneric) - require.NoError(t, err) - require.Equal(t, circuit.Data, []byte("linux")) - require.Equal(t, remote.Data, []byte("boot")) -} + // Empty. + require.Nil(t, GetRelayAgentInfo(Options{})) -func TestParseOptRelayAgentInformationToBytes(t *testing.T) { - opt := OptRelayAgentInformation{ - Options: Options{ - &OptionGeneric{OptionCode: optionCode(1), Data: []byte("linux")}, - &OptionGeneric{OptionCode: optionCode(2), Data: []byte("boot")}, + // Invalid contents. + o = Options{ + OptionRelayAgentInformation.Code(): []byte{ + 1, 7, 'l', 'i', 'n', 'u', 'x', }, } - data := opt.ToBytes() - expected := []byte{ + require.Nil(t, GetRelayAgentInfo(o)) +} + +func TestOptRelayAgentInfo(t *testing.T) { + opt := OptRelayAgentInfo( + OptGeneric(GenericOptionCode(1), []byte("linux")), + OptGeneric(GenericOptionCode(2), []byte("boot")), + ) + wantBytes := []byte{ 1, 5, 'l', 'i', 'n', 'u', 'x', 2, 4, 'b', 'o', 'o', 't', } - require.Equal(t, expected, data) -} - -func TestOptRelayAgentInformationToBytesString(t *testing.T) { - o := OptRelayAgentInformation{} - require.Equal(t, "Relay Agent Information -> []", o.String()) + wantString := "Relay Agent Information:\n unknown (1): [108 105 110 117 120]\n unknown (2): [98 111 111 116]\n" + require.Equal(t, wantBytes, opt.Value.ToBytes()) + require.Equal(t, OptionRelayAgentInformation, opt.Code) + require.Equal(t, wantString, opt.String()) } diff --git a/dhcpv4/option_string.go b/dhcpv4/option_string.go index 76a2db2..9e16d6c 100644 --- a/dhcpv4/option_string.go +++ b/dhcpv4/option_string.go @@ -1,163 +1,117 @@ package dhcpv4 -import ( - "fmt" -) - -// OptDomainName implements the domain name option described in RFC 2132, -// Section 3.17. -type OptDomainName struct { - DomainName string -} - -// ParseOptDomainName returns a new OptDomainName from a byte stream, or error -// if any. -func ParseOptDomainName(data []byte) (*OptDomainName, error) { - return &OptDomainName{DomainName: string(data)}, nil -} - -// Code returns the option code. -func (o *OptDomainName) Code() OptionCode { - return OptionDomainName -} - -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptDomainName) ToBytes() []byte { - return []byte(o.DomainName) -} - -// String returns a human-readable string. -func (o *OptDomainName) String() string { - return fmt.Sprintf("Domain Name -> %v", o.DomainName) -} - -// OptHostName implements the host name option described by RFC 2132, Section -// 3.14. -type OptHostName struct { - HostName string -} - -// ParseOptHostName returns a new OptHostName from a byte stream, or error if -// any. -func ParseOptHostName(data []byte) (*OptHostName, error) { - return &OptHostName{HostName: string(data)}, nil -} - -// Code returns the option code. -func (o *OptHostName) Code() OptionCode { - return OptionHostName -} +// String represents an option encapsulating a string in IPv4 DHCP. +// +// This representation is shared by multiple options specified by RFC 2132, +// Sections 3.14, 3.16, 3.17, 3.19, and 3.20. +type String string // ToBytes returns a serialized stream of bytes for this option. -func (o *OptHostName) ToBytes() []byte { - return []byte(o.HostName) +func (o String) ToBytes() []byte { + return []byte(o) } // String returns a human-readable string. -func (o *OptHostName) String() string { - return fmt.Sprintf("Host Name -> %v", o.HostName) -} - -// OptRootPath implements the root path option described by RFC 2132, Section -// 3.19. -type OptRootPath struct { - Path string -} - -// ParseOptRootPath constructs an OptRootPath struct from a sequence of bytes -// and returns it, or an error. -func ParseOptRootPath(data []byte) (*OptRootPath, error) { - return &OptRootPath{Path: string(data)}, nil -} - -// Code returns the option code. -func (o *OptRootPath) Code() OptionCode { - return OptionRootPath +func (o String) String() string { + return string(o) } -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptRootPath) ToBytes() []byte { - return []byte(o.Path) -} - -// String returns a human-readable string for this option. -func (o *OptRootPath) String() string { - return fmt.Sprintf("Root Path -> %v", o.Path) -} - -// OptBootfileName implements the bootfile name option described in RFC 2132, -// Section 9.5. -type OptBootfileName struct { - BootfileName string -} - -// Code returns the option code -func (op *OptBootfileName) Code() OptionCode { - return OptionBootfileName +// FromBytes parses a serialized stream of bytes into o. +func (o *String) FromBytes(data []byte) error { + *o = String(string(data)) + return nil } -// ToBytes serializes the option and returns it as a sequence of bytes -func (op *OptBootfileName) ToBytes() []byte { - return []byte(op.BootfileName) +// GetString parses an RFC 2132 string from o[code]. +func GetString(code OptionCode, o Options) string { + v := o.Get(code) + if v == nil { + return "" + } + return string(v) } -func (op *OptBootfileName) String() string { - return fmt.Sprintf("Bootfile Name -> %s", op.BootfileName) +// OptDomainName returns a new DHCPv4 Domain Name option. +// +// The Domain Name option is described by RFC 2132, Section 3.17. +func OptDomainName(name string) Option { + return Option{Code: OptionDomainName, Value: String(name)} } -// ParseOptBootfileName returns a new OptBootfile from a byte stream or error if any -func ParseOptBootfileName(data []byte) (*OptBootfileName, error) { - return &OptBootfileName{BootfileName: string(data)}, nil +// GetDomainName parses the DHCPv4 Domain Name option from o if present. +// +// The Domain Name option is described by RFC 2132, Section 3.17. +func GetDomainName(o Options) string { + return GetString(OptionDomainName, o) } -// OptTFTPServerName implements the TFTP server name option described by RFC -// 2132, Section 9.4. -type OptTFTPServerName struct { - TFTPServerName string +// OptHostName returns a new DHCPv4 Host Name option. +// +// The Host Name option is described by RFC 2132, Section 3.14. +func OptHostName(name string) Option { + return Option{Code: OptionHostName, Value: String(name)} } -// Code returns the option code -func (op *OptTFTPServerName) Code() OptionCode { - return OptionTFTPServerName +// GetHostName parses the DHCPv4 Host Name option from o if present. +// +// The Host Name option is described by RFC 2132, Section 3.14. +func GetHostName(o Options) string { + return GetString(OptionHostName, o) } -// ToBytes serializes the option and returns it as a sequence of bytes -func (op *OptTFTPServerName) ToBytes() []byte { - return []byte(op.TFTPServerName) +// OptRootPath returns a new DHCPv4 Root Path option. +// +// The Root Path option is described by RFC 2132, Section 3.19. +func OptRootPath(name string) Option { + return Option{Code: OptionRootPath, Value: String(name)} } -func (op *OptTFTPServerName) String() string { - return fmt.Sprintf("TFTP Server Name -> %s", op.TFTPServerName) +// GetRootPath parses the DHCPv4 Root Path option from o if present. +// +// The Root Path option is described by RFC 2132, Section 3.19. +func GetRootPath(o Options) string { + return GetString(OptionRootPath, o) } -// ParseOptTFTPServerName returns a new OptTFTPServerName from a byte stream or error if any -func ParseOptTFTPServerName(data []byte) (*OptTFTPServerName, error) { - return &OptTFTPServerName{TFTPServerName: string(data)}, nil +// OptBootFileName returns a new DHCPv4 Boot File Name option. +// +// The Bootfile Name option is described by RFC 2132, Section 9.5. +func OptBootFileName(name string) Option { + return Option{Code: OptionBootfileName, Value: String(name)} } -// OptClassIdentifier implements the vendor class identifier option described -// in RFC 2132, Section 9.13. -type OptClassIdentifier struct { - Identifier string +// GetBootFileName parses the DHCPv4 Bootfile Name option from o if present. +// +// The Bootfile Name option is described by RFC 2132, Section 9.5. +func GetBootFileName(o Options) string { + return GetString(OptionBootfileName, o) } -// ParseOptClassIdentifier constructs an OptClassIdentifier struct from a sequence of -// bytes and returns it, or an error. -func ParseOptClassIdentifier(data []byte) (*OptClassIdentifier, error) { - return &OptClassIdentifier{Identifier: string(data)}, nil +// OptTFTPServerName returns a new DHCPv4 TFTP Server Name option. +// +// The TFTP Server Name option is described by RFC 2132, Section 9.4. +func OptTFTPServerName(name string) Option { + return Option{Code: OptionTFTPServerName, Value: String(name)} } -// Code returns the option code. -func (o *OptClassIdentifier) Code() OptionCode { - return OptionClassIdentifier +// GetTFTPServerName parses the DHCPv4 TFTP Server Name option from o if +// present. +// +// The TFTP Server Name option is described by RFC 2132, Section 9.4. +func GetTFTPServerName(o Options) string { + return GetString(OptionTFTPServerName, o) } -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptClassIdentifier) ToBytes() []byte { - return []byte(o.Identifier) +// OptClassIdentifier returns a new DHCPv4 Class Identifier option. +// +// The Vendor Class Identifier option is described by RFC 2132, Section 9.13. +func OptClassIdentifier(name string) Option { + return Option{Code: OptionClassIdentifier, Value: String(name)} } -// String returns a human-readable string for this option. -func (o *OptClassIdentifier) String() string { - return fmt.Sprintf("Class Identifier -> %v", o.Identifier) +// GetClassIdentifier parses the DHCPv4 Class Identifier option from o if present. +// +// The Vendor Class Identifier option is described by RFC 2132, Section 9.13. +func GetClassIdentifier(o Options) string { + return GetString(OptionClassIdentifier, o) } diff --git a/dhcpv4/option_string_test.go b/dhcpv4/option_string_test.go index 0704f31..bda6009 100644 --- a/dhcpv4/option_string_test.go +++ b/dhcpv4/option_string_test.go @@ -7,96 +7,83 @@ import ( ) func TestOptDomainName(t *testing.T) { - o := OptDomainName{DomainName: "foo"} - require.Equal(t, OptionDomainName, o.Code(), "Code") - require.Equal(t, []byte{'f', 'o', 'o'}, o.ToBytes(), "ToBytes") - require.Equal(t, "Domain Name -> foo", o.String()) + o := OptDomainName("foo") + require.Equal(t, OptionDomainName, o.Code, "Code") + require.Equal(t, []byte{'f', 'o', 'o'}, o.Value.ToBytes(), "ToBytes") + require.Equal(t, "Domain Name: foo", o.String()) } func TestParseOptDomainName(t *testing.T) { - data := []byte{'t', 'e', 's', 't'} - o, err := ParseOptDomainName(data) - require.NoError(t, err) - require.Equal(t, &OptDomainName{DomainName: "test"}, o) + o := Options{ + OptionDomainName.Code(): []byte{'t', 'e', 's', 't'}, + } + require.Equal(t, "test", GetDomainName(o)) + require.Equal(t, "", GetDomainName(Options{})) } func TestOptHostName(t *testing.T) { - o := OptHostName{HostName: "foo"} - require.Equal(t, OptionHostName, o.Code(), "Code") - require.Equal(t, []byte{'f', 'o', 'o'}, o.ToBytes(), "ToBytes") - require.Equal(t, "Host Name -> foo", o.String()) + o := OptHostName("foo") + require.Equal(t, OptionHostName, o.Code, "Code") + require.Equal(t, []byte{'f', 'o', 'o'}, o.Value.ToBytes(), "ToBytes") + require.Equal(t, "Host Name: foo", o.String()) } func TestParseOptHostName(t *testing.T) { - data := []byte{'t', 'e', 's', 't'} - o, err := ParseOptHostName(data) - require.NoError(t, err) - require.Equal(t, &OptHostName{HostName: "test"}, o) + o := Options{ + OptionHostName.Code(): []byte{'t', 'e', 's', 't'}, + } + require.Equal(t, "test", GetHostName(o)) + require.Equal(t, "", GetHostName(Options{})) } func TestOptRootPath(t *testing.T) { - o := OptRootPath{Path: "/foo/bar/baz"} - require.Equal(t, OptionRootPath, o.Code(), "Code") - wantBytes := []byte{ - '/', 'f', 'o', 'o', '/', 'b', 'a', 'r', '/', 'b', 'a', 'z', - } - require.Equal(t, wantBytes, o.ToBytes(), "ToBytes") - require.Equal(t, "Root Path -> /foo/bar/baz", o.String()) + o := OptRootPath("foo") + require.Equal(t, OptionRootPath, o.Code, "Code") + require.Equal(t, []byte{'f', 'o', 'o'}, o.Value.ToBytes(), "ToBytes") + require.Equal(t, "Root Path: foo", o.String()) } func TestParseOptRootPath(t *testing.T) { - data := []byte{byte(OptionRootPath), 4, '/', 'f', 'o', 'o'} - o, err := ParseOptRootPath(data[2:]) - require.NoError(t, err) - require.Equal(t, &OptRootPath{Path: "/foo"}, o) + o := OptionsFromList(OptRootPath("test")) + require.Equal(t, "test", GetRootPath(o)) + require.Equal(t, "", GetRootPath(Options{})) } -func TestOptBootfileName(t *testing.T) { - opt := OptBootfileName{ - BootfileName: "linuxboot", - } - require.Equal(t, OptionBootfileName, opt.Code()) - require.Equal(t, []byte{'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't'}, opt.ToBytes()) - require.Equal(t, "Bootfile Name -> linuxboot", opt.String()) +func TestOptBootFileName(t *testing.T) { + o := OptBootFileName("foo") + require.Equal(t, OptionBootfileName, o.Code, "Code") + require.Equal(t, []byte{'f', 'o', 'o'}, o.Value.ToBytes(), "ToBytes") + require.Equal(t, "Bootfile Name: foo", o.String()) } -func TestParseOptBootfileName(t *testing.T) { - expected := []byte{ - 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', - } - opt, err := ParseOptBootfileName(expected) - require.NoError(t, err) - require.Equal(t, "linuxboot", opt.BootfileName) +func TestParseOptBootFileName(t *testing.T) { + o := OptionsFromList(OptBootFileName("test")) + require.Equal(t, "test", GetBootFileName(o)) + require.Equal(t, "", GetBootFileName(Options{})) } -func TestOptTFTPServer(t *testing.T) { - opt := OptTFTPServerName{ - TFTPServerName: "linuxboot", - } - require.Equal(t, OptionTFTPServerName, opt.Code()) - require.Equal(t, []byte("linuxboot"), opt.ToBytes()) - require.Equal(t, "TFTP Server Name -> linuxboot", opt.String()) +func TestOptTFTPServerName(t *testing.T) { + o := OptTFTPServerName("foo") + require.Equal(t, OptionTFTPServerName, o.Code, "Code") + require.Equal(t, []byte{'f', 'o', 'o'}, o.Value.ToBytes(), "ToBytes") + require.Equal(t, "TFTP Server Name: foo", o.String()) } func TestParseOptTFTPServerName(t *testing.T) { - expected := []byte{ - 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', - } - opt, err := ParseOptTFTPServerName(expected) - require.NoError(t, err) - require.Equal(t, "linuxboot", string(opt.TFTPServerName)) + o := OptionsFromList(OptTFTPServerName("test")) + require.Equal(t, "test", GetTFTPServerName(o)) + require.Equal(t, "", GetTFTPServerName(Options{})) } func TestOptClassIdentifier(t *testing.T) { - o := OptClassIdentifier{Identifier: "foo"} - require.Equal(t, OptionClassIdentifier, o.Code(), "Code") - require.Equal(t, []byte("foo"), o.ToBytes(), "ToBytes") - require.Equal(t, "Class Identifier -> foo", o.String()) + o := OptClassIdentifier("foo") + require.Equal(t, OptionClassIdentifier, o.Code, "Code") + require.Equal(t, []byte{'f', 'o', 'o'}, o.Value.ToBytes(), "ToBytes") + require.Equal(t, "Class Identifier: foo", o.String()) } func TestParseOptClassIdentifier(t *testing.T) { - data := []byte("test") - o, err := ParseOptClassIdentifier(data) - require.NoError(t, err) - require.Equal(t, &OptClassIdentifier{Identifier: "test"}, o) + o := OptionsFromList(OptClassIdentifier("test")) + require.Equal(t, "test", GetClassIdentifier(o)) + require.Equal(t, "", GetClassIdentifier(Options{})) } diff --git a/dhcpv4/option_subnet_mask.go b/dhcpv4/option_subnet_mask.go index 19401d8..82b344b 100644 --- a/dhcpv4/option_subnet_mask.go +++ b/dhcpv4/option_subnet_mask.go @@ -1,36 +1,55 @@ package dhcpv4 import ( - "fmt" "net" "github.com/u-root/u-root/pkg/uio" ) -// OptSubnetMask implements the subnet mask option described by RFC 2132, -// Section 3.3. -type OptSubnetMask struct { - SubnetMask net.IPMask +// IPMask represents an option encapsulating the subnet mask. +// +// This option implements the subnet mask option in RFC 2132, Section 3.3. +type IPMask net.IPMask + +// ToBytes returns a serialized stream of bytes for this option. +func (im IPMask) ToBytes() []byte { + if len(im) > net.IPv4len { + return im[:net.IPv4len] + } + return im } -// ParseOptSubnetMask returns a new OptSubnetMask from a byte -// stream, or error if any. -func ParseOptSubnetMask(data []byte) (*OptSubnetMask, error) { - buf := uio.NewBigEndianBuffer(data) - return &OptSubnetMask{SubnetMask: net.IPMask(buf.CopyN(net.IPv4len))}, buf.FinError() +// String returns a human-readable string. +func (im IPMask) String() string { + return net.IPMask(im).String() } -// Code returns the option code. -func (o *OptSubnetMask) Code() OptionCode { - return OptionSubnetMask +// FromBytes parses im from data per RFC 2132. +func (im *IPMask) FromBytes(data []byte) error { + buf := uio.NewBigEndianBuffer(data) + *im = IPMask(buf.CopyN(net.IPv4len)) + return buf.FinError() } -// ToBytes returns a serialized stream of bytes for this option. -func (o *OptSubnetMask) ToBytes() []byte { - return o.SubnetMask[:net.IPv4len] +// GetSubnetMask returns a subnet mask option contained in o, if there is one. +// +// The subnet mask option is described by RFC 2132, Section 3.3. +func GetSubnetMask(o Options) net.IPMask { + v := o.Get(OptionSubnetMask) + if v == nil { + return nil + } + var im IPMask + if err := im.FromBytes(v); err != nil { + return nil + } + return net.IPMask(im) } -// String returns a human-readable string. -func (o *OptSubnetMask) String() string { - return fmt.Sprintf("Subnet Mask -> %v", o.SubnetMask.String()) +// OptSubnetMask returns a new DHCPv4 SubnetMask option per RFC 2132, Section 3.3. +func OptSubnetMask(mask net.IPMask) Option { + return Option{ + Code: OptionSubnetMask, + Value: IPMask(mask), + } } diff --git a/dhcpv4/option_subnet_mask_test.go b/dhcpv4/option_subnet_mask_test.go index f04a481..bc82cf1 100644 --- a/dhcpv4/option_subnet_mask_test.go +++ b/dhcpv4/option_subnet_mask_test.go @@ -7,30 +7,23 @@ import ( "github.com/stretchr/testify/require" ) -func TestOptSubnetMaskInterfaceMethods(t *testing.T) { - mask := net.IPMask{255, 255, 255, 0} - o := OptSubnetMask{SubnetMask: mask} - - require.Equal(t, OptionSubnetMask, o.Code(), "Code") - - expectedBytes := []byte{255, 255, 255, 0} - require.Equal(t, expectedBytes, o.ToBytes(), "ToBytes") - - require.Equal(t, "Subnet Mask -> ffffff00", o.String(), "String") +func TestOptSubnetMask(t *testing.T) { + o := OptSubnetMask(net.IPMask{255, 255, 255, 0}) + require.Equal(t, o.Code, OptionSubnetMask, "Code") + require.Equal(t, "Subnet Mask: ffffff00", o.String(), "String") + require.Equal(t, []byte{255, 255, 255, 0}, o.Value.ToBytes(), "ToBytes") } -func TestParseOptSubnetMask(t *testing.T) { - var ( - o *OptSubnetMask - err error - ) - o, err = ParseOptSubnetMask([]byte{}) - require.Error(t, err, "empty byte stream") +func TestGetSubnetMask(t *testing.T) { + o := OptionsFromList(OptSubnetMask(net.IPMask{})) + mask := GetSubnetMask(o) + require.Nil(t, mask, "empty byte stream") - o, err = ParseOptSubnetMask([]byte{255}) - require.Error(t, err, "short byte stream") + o = OptionsFromList(OptSubnetMask(net.IPMask{255})) + mask = GetSubnetMask(o) + require.Nil(t, mask, "short byte stream") - o, err = ParseOptSubnetMask([]byte{255, 255, 255, 0}) - require.NoError(t, err) - require.Equal(t, net.IPMask{255, 255, 255, 0}, o.SubnetMask) + o = OptionsFromList(OptSubnetMask(net.IPMask{255, 255, 255, 0})) + mask = GetSubnetMask(o) + require.Equal(t, net.IPMask{255, 255, 255, 0}, mask) } diff --git a/dhcpv4/option_userclass.go b/dhcpv4/option_userclass.go index 110cb37..f273a84 100644 --- a/dhcpv4/option_userclass.go +++ b/dhcpv4/option_userclass.go @@ -8,21 +8,53 @@ import ( "github.com/u-root/u-root/pkg/uio" ) -// OptUserClass implements the user class option described by RFC 3004. -type OptUserClass struct { +// UserClass implements the user class option described by RFC 3004. +type UserClass struct { UserClasses [][]byte - Rfc3004 bool + RFC3004 bool } -// Code returns the option code -func (op *OptUserClass) Code() OptionCode { - return OptionUserClassInformation +// GetUserClass returns the user class in o if present. +// +// The user class information option is defined by RFC 3004. +func GetUserClass(o Options) *UserClass { + v := o.Get(OptionUserClassInformation) + if v == nil { + return nil + } + var uc UserClass + if err := uc.FromBytes(v); err != nil { + return nil + } + return &uc +} + +// OptUserClass returns a new user class option. +func OptUserClass(v []byte) Option { + return Option{ + Code: OptionUserClassInformation, + Value: &UserClass{ + UserClasses: [][]byte{v}, + RFC3004: false, + }, + } +} + +// OptRFC3004UserClass returns a new user class option according to RFC 3004. +func OptRFC3004UserClass(v [][]byte) Option { + return Option{ + Code: OptionUserClassInformation, + Value: &UserClass{ + UserClasses: v, + RFC3004: true, + }, + } } // ToBytes serializes the option and returns it as a sequence of bytes -func (op *OptUserClass) ToBytes() []byte { +func (op *UserClass) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) - if !op.Rfc3004 { + if !op.RFC3004 { buf.WriteBytes(op.UserClasses[0]) } else { for _, uc := range op.UserClasses { @@ -33,22 +65,21 @@ func (op *OptUserClass) ToBytes() []byte { return buf.Data() } -func (op *OptUserClass) String() string { +// String returns a human-readable user class. +func (op *UserClass) String() string { ucStrings := make([]string, 0, len(op.UserClasses)) - if !op.Rfc3004 { + if !op.RFC3004 { ucStrings = append(ucStrings, string(op.UserClasses[0])) } else { for _, uc := range op.UserClasses { ucStrings = append(ucStrings, string(uc)) } } - return fmt.Sprintf("User Class Information -> %v", strings.Join(ucStrings, ", ")) + return strings.Join(ucStrings, ", ") } -// ParseOptUserClass returns a new OptUserClass from a byte stream or -// error if any -func ParseOptUserClass(data []byte) (*OptUserClass, error) { - opt := OptUserClass{} +// FromBytes parses data into op. +func (op *UserClass) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) // Check if option is Microsoft style instead of RFC compliant, issue #113 @@ -64,19 +95,19 @@ func ParseOptUserClass(data []byte) (*OptUserClass, error) { counting += int(data[counting]) + 1 } if counting != buf.Len() { - opt.UserClasses = append(opt.UserClasses, data) - return &opt, nil + op.UserClasses = append(op.UserClasses, data) + return nil } - opt.Rfc3004 = true + op.RFC3004 = true for buf.Has(1) { ucLen := buf.Read8() if ucLen == 0 { - return nil, fmt.Errorf("DHCP user class must have length greater than 0") + return fmt.Errorf("DHCP user class must have length greater than 0") } - opt.UserClasses = append(opt.UserClasses, buf.CopyN(int(ucLen))) + op.UserClasses = append(op.UserClasses, buf.CopyN(int(ucLen))) } - if len(opt.UserClasses) == 0 { - return nil, errors.New("ParseOptUserClass: at least one user class is required") + if len(op.UserClasses) == 0 { + return errors.New("ParseOptUserClass: at least one user class is required") } - return &opt, buf.FinError() + return buf.FinError() } diff --git a/dhcpv4/option_userclass_test.go b/dhcpv4/option_userclass_test.go index e321a64..149fb92 100644 --- a/dhcpv4/option_userclass_test.go +++ b/dhcpv4/option_userclass_test.go @@ -7,11 +7,8 @@ import ( ) func TestOptUserClassToBytes(t *testing.T) { - opt := OptUserClass{ - UserClasses: [][]byte{[]byte("linuxboot")}, - Rfc3004: true, - } - data := opt.ToBytes() + opt := OptRFC3004UserClass([][]byte{[]byte("linuxboot")}) + data := opt.Value.ToBytes() expected := []byte{ 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', } @@ -19,10 +16,8 @@ func TestOptUserClassToBytes(t *testing.T) { } func TestOptUserClassMicrosoftToBytes(t *testing.T) { - opt := OptUserClass{ - UserClasses: [][]byte{[]byte("linuxboot")}, - } - data := opt.ToBytes() + opt := OptUserClass([]byte("linuxboot")) + data := opt.Value.ToBytes() expected := []byte{ 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', } @@ -30,11 +25,12 @@ func TestOptUserClassMicrosoftToBytes(t *testing.T) { } func TestParseOptUserClassMultiple(t *testing.T) { + var opt UserClass expected := []byte{ 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 4, 't', 'e', 's', 't', } - opt, err := ParseOptUserClass(expected) + err := opt.FromBytes(expected) require.NoError(t, err) require.Equal(t, len(opt.UserClasses), 2) require.Equal(t, []byte("linuxboot"), opt.UserClasses[0]) @@ -42,50 +38,53 @@ func TestParseOptUserClassMultiple(t *testing.T) { } func TestParseOptUserClassNone(t *testing.T) { + var opt UserClass expected := []byte{} - _, err := ParseOptUserClass(expected) + err := opt.FromBytes(expected) require.Error(t, err) } func TestParseOptUserClassMicrosoft(t *testing.T) { + var opt UserClass expected := []byte{ 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', } - opt, err := ParseOptUserClass(expected) + err := opt.FromBytes(expected) require.NoError(t, err) require.Equal(t, 1, len(opt.UserClasses)) require.Equal(t, []byte("linuxboot"), opt.UserClasses[0]) } func TestParseOptUserClassMicrosoftShort(t *testing.T) { + var opt UserClass expected := []byte{ 'l', } - opt, err := ParseOptUserClass(expected) + err := opt.FromBytes(expected) require.NoError(t, err) require.Equal(t, 1, len(opt.UserClasses)) require.Equal(t, []byte("l"), opt.UserClasses[0]) } func TestParseOptUserClass(t *testing.T) { + var opt UserClass expected := []byte{ 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', } - opt, err := ParseOptUserClass(expected) + err := opt.FromBytes(expected) require.NoError(t, err) require.Equal(t, 1, len(opt.UserClasses)) require.Equal(t, []byte("linuxboot"), opt.UserClasses[0]) } func TestOptUserClassToBytesMultiple(t *testing.T) { - opt := OptUserClass{ - UserClasses: [][]byte{ + opt := OptRFC3004UserClass( + [][]byte{ []byte("linuxboot"), []byte("test"), }, - Rfc3004: true, - } - data := opt.ToBytes() + ) + data := opt.Value.ToBytes() expected := []byte{ 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 4, 't', 'e', 's', 't', @@ -94,14 +93,7 @@ func TestOptUserClassToBytesMultiple(t *testing.T) { } func TestParseOptUserClassZeroLength(t *testing.T) { - expected := []byte{ - 0, 0, - } - _, err := ParseOptUserClass(expected) + var opt UserClass + err := opt.FromBytes([]byte{0, 0}) require.Error(t, err) } - -func TestOptUserClassCode(t *testing.T) { - opt := OptUserClass{} - require.Equal(t, OptionUserClassInformation, opt.Code()) -} diff --git a/dhcpv4/option_vivc.go b/dhcpv4/option_vivc.go index b6efab9..509ba80 100644 --- a/dhcpv4/option_vivc.go +++ b/dhcpv4/option_vivc.go @@ -10,39 +10,53 @@ import ( // VIVCIdentifier implements the vendor-identifying vendor class option // described by RFC 3925. type VIVCIdentifier struct { + // EntID is the enterprise ID. EntID uint32 Data []byte } -// OptVIVC represents the DHCP message type option. -type OptVIVC struct { - Identifiers []VIVCIdentifier +// OptVIVC returns a new vendor-identifying vendor class option. +// +// The option is described by RFC 3925. +func OptVIVC(identifiers ...VIVCIdentifier) Option { + return Option{ + Code: OptionVendorIdentifyingVendorClass, + Value: VIVCIdentifiers(identifiers), + } } -// ParseOptVIVC contructs an OptVIVC tsruct from a sequence of bytes and returns -// it, or an error. -func ParseOptVIVC(data []byte) (*OptVIVC, error) { - buf := uio.NewBigEndianBuffer(data) +// GetVIVC returns the vendor-identifying vendor class option in o if present. +func GetVIVC(o Options) VIVCIdentifiers { + v := o.Get(OptionVendorIdentifyingVendorClass) + if v == nil { + return nil + } + var ids VIVCIdentifiers + if err := ids.FromBytes(v); err != nil { + return nil + } + return ids +} + +// VIVCIdentifiers implements encoding and decoding methods for a DHCP option +// described in RFC 3925. +type VIVCIdentifiers []VIVCIdentifier - var ids []VIVCIdentifier +// FromBytes parses data into ids per RFC 3925. +func (ids *VIVCIdentifiers) FromBytes(data []byte) error { + buf := uio.NewBigEndianBuffer(data) for buf.Has(5) { entID := buf.Read32() idLen := int(buf.Read8()) - ids = append(ids, VIVCIdentifier{EntID: entID, Data: buf.CopyN(idLen)}) + *ids = append(*ids, VIVCIdentifier{EntID: entID, Data: buf.CopyN(idLen)}) } - - return &OptVIVC{Identifiers: ids}, buf.FinError() -} - -// Code returns the option code. -func (o *OptVIVC) Code() OptionCode { - return OptionVendorIdentifyingVendorClass + return buf.FinError() } // ToBytes returns a serialized stream of bytes for this option. -func (o *OptVIVC) ToBytes() []byte { +func (ids VIVCIdentifiers) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) - for _, id := range o.Identifiers { + for _, id := range ids { buf.Write32(id.EntID) buf.Write8(uint8(len(id.Data))) buf.WriteBytes(id.Data) @@ -51,13 +65,13 @@ func (o *OptVIVC) ToBytes() []byte { } // String returns a human-readable string for this option. -func (o *OptVIVC) String() string { +func (ids VIVCIdentifiers) String() string { + if len(ids) == 0 { + return "" + } buf := bytes.Buffer{} - fmt.Fprintf(&buf, "Vendor-Identifying Vendor Class ->") - - for _, id := range o.Identifiers { + for _, id := range ids { fmt.Fprintf(&buf, " %d:'%s',", id.EntID, id.Data) } - - return buf.String()[:buf.Len()-1] + return buf.String()[1 : buf.Len()-1] } diff --git a/dhcpv4/option_vivc_test.go b/dhcpv4/option_vivc_test.go index 9b3b704..b1ec398 100644 --- a/dhcpv4/option_vivc_test.go +++ b/dhcpv4/option_vivc_test.go @@ -7,11 +7,9 @@ import ( ) var ( - sampleVIVCOpt = OptVIVC{ - Identifiers: []VIVCIdentifier{ - {EntID: 9, Data: []byte("CiscoIdentifier")}, - {EntID: 18, Data: []byte("WellfleetIdentifier")}, - }, + sampleVIVCOpt = VIVCIdentifiers{ + VIVCIdentifier{EntID: 9, Data: []byte("CiscoIdentifier")}, + VIVCIdentifier{EntID: 18, Data: []byte("WellfleetIdentifier")}, } sampleVIVCOptRaw = []byte{ 0x0, 0x0, 0x0, 0x9, // enterprise id 9 @@ -24,30 +22,31 @@ var ( ) func TestOptVIVCInterfaceMethods(t *testing.T) { - require.Equal(t, OptionVendorIdentifyingVendorClass, sampleVIVCOpt.Code(), "Code") - require.Equal(t, sampleVIVCOptRaw, sampleVIVCOpt.ToBytes(), "ToBytes") + opt := OptVIVC(sampleVIVCOpt...) + require.Equal(t, OptionVendorIdentifyingVendorClass, opt.Code, "Code") + require.Equal(t, sampleVIVCOptRaw, opt.Value.ToBytes(), "ToBytes") + require.Equal(t, "Vendor-Identifying Vendor Class: 9:'CiscoIdentifier', 18:'WellfleetIdentifier'", + opt.String()) } func TestParseOptVICO(t *testing.T) { - o, err := ParseOptVIVC(sampleVIVCOptRaw) - require.NoError(t, err) - require.Equal(t, &sampleVIVCOpt, o) + options := Options{OptionVendorIdentifyingVendorClass.Code(): sampleVIVCOptRaw} + o := GetVIVC(options) + require.Equal(t, sampleVIVCOpt, o) // Identifier len too long data := make([]byte, len(sampleVIVCOptRaw)) copy(data, sampleVIVCOptRaw) data[4] = 40 - _, err = ParseOptVIVC(data) - require.Error(t, err, "should get error from bad length") + options = Options{OptionVendorIdentifyingVendorClass.Code(): data} + o = GetVIVC(options) + require.Nil(t, o, "should get error from bad length") // Longer than length data[4] = 5 - o, err = ParseOptVIVC(data[:10]) - require.NoError(t, err) - require.Equal(t, o.Identifiers[0].Data, []byte("Cisco")) -} + options = Options{OptionVendorIdentifyingVendorClass.Code(): data[:10]} + o = GetVIVC(options) + require.Equal(t, o[0].Data, []byte("Cisco")) -func TestOptVIVCString(t *testing.T) { - require.Equal(t, "Vendor-Identifying Vendor Class -> 9:'CiscoIdentifier', 18:'WellfleetIdentifier'", - sampleVIVCOpt.String()) + require.Equal(t, VIVCIdentifiers(nil), GetVIVC(Options{})) } diff --git a/dhcpv4/options.go b/dhcpv4/options.go index 3d774d1..4c70743 100644 --- a/dhcpv4/options.go +++ b/dhcpv4/options.go @@ -5,7 +5,11 @@ import ( "fmt" "io" "math" + "sort" + "strings" + "github.com/insomniacslk/dhcp/iana" + "github.com/insomniacslk/dhcp/rfc1035label" "github.com/u-root/u-root/pkg/uio" ) @@ -24,149 +28,95 @@ var ( ErrInvalidOptions = errors.New("invalid options data") ) -// Option is an interface that all DHCP v4 options adhere to. -type Option interface { - Code() OptionCode +// OptionValue is an interface that all DHCP v4 options adhere to. +type OptionValue interface { ToBytes() []byte String() string } -// ParseOption parses a sequence of bytes as a single DHCPv4 option, returning -// the specific option structure or error, if any. -func ParseOption(code OptionCode, data []byte) (Option, error) { - var ( - opt Option - err error - ) - switch code { - case OptionSubnetMask: - opt, err = ParseOptSubnetMask(data) - case OptionRouter: - opt, err = ParseOptRouter(data) - case OptionDomainNameServer: - opt, err = ParseOptDomainNameServer(data) - case OptionHostName: - opt, err = ParseOptHostName(data) - case OptionDomainName: - opt, err = ParseOptDomainName(data) - case OptionRootPath: - opt, err = ParseOptRootPath(data) - case OptionBroadcastAddress: - opt, err = ParseOptBroadcastAddress(data) - case OptionNTPServers: - opt, err = ParseOptNTPServers(data) - case OptionRequestedIPAddress: - opt, err = ParseOptRequestedIPAddress(data) - case OptionIPAddressLeaseTime: - opt, err = ParseOptIPAddressLeaseTime(data) - case OptionDHCPMessageType: - opt, err = ParseOptMessageType(data) - case OptionServerIdentifier: - opt, err = ParseOptServerIdentifier(data) - case OptionParameterRequestList: - opt, err = ParseOptParameterRequestList(data) - case OptionMaximumDHCPMessageSize: - opt, err = ParseOptMaximumDHCPMessageSize(data) - case OptionClassIdentifier: - opt, err = ParseOptClassIdentifier(data) - case OptionTFTPServerName: - opt, err = ParseOptTFTPServerName(data) - case OptionBootfileName: - opt, err = ParseOptBootfileName(data) - case OptionUserClassInformation: - opt, err = ParseOptUserClass(data) - case OptionRelayAgentInformation: - opt, err = ParseOptRelayAgentInformation(data) - case OptionClientSystemArchitectureType: - opt, err = ParseOptClientArchType(data) - case OptionDNSDomainSearchList: - opt, err = ParseOptDomainSearch(data) - case OptionVendorIdentifyingVendorClass: - opt, err = ParseOptVIVC(data) - default: - opt, err = ParseOptionGeneric(code, data) - } - if err != nil { - return nil, err +// Option is a DHCPv4 option and consists of a 1-byte option code and a value +// stream of bytes. +// +// The value is to be interpreted based on the option code. +type Option struct { + Code OptionCode + Value OptionValue +} + +// String returns a human-readable version of this option. +func (o Option) String() string { + v := o.Value.String() + if strings.Contains(v, "\n") { + return fmt.Sprintf("%s:\n%s", o.Code, v) } - return opt, nil + return fmt.Sprintf("%s: %s", o.Code, v) } // Options is a collection of options. -type Options []Option +type Options map[uint8][]byte -// GetOne will attempt to get an option that match a Option code. If there -// are multiple options with the same OptionCode it will only return the first -// one found. If no matching option is found nil will be returned. -func (o Options) GetOne(code OptionCode) Option { +// OptionsFromList adds all given options to an options map. +func OptionsFromList(o ...Option) Options { + opts := make(Options) for _, opt := range o { - if opt.Code() == code { - return opt - } + opts.Update(opt) } - return nil + return opts } -// Has checks whether o has the given `opcode` Option. -func (o Options) Has(code OptionCode) bool { - return o.GetOne(code) != nil +// Get will attempt to get all options that match a DHCPv4 option +// from its OptionCode. If the option was not found it will return an +// empty list. +// +// According to RFC 3396, options that are specified more than once are +// concatenated, and hence this should always just return one option. This +// currently returns a list to be API compatible. +func (o Options) Get(code OptionCode) []byte { + return o[code.Code()] } -// Update replaces an existing option with the same option code with the given -// one, adding it if not already present. -// -// Per RFC 2131, Section 4.1, "options may appear only once." -// -// An End option is ignored. -func (o *Options) Update(option Option) { - if option.Code() == OptionEnd { - return - } +// Has checks whether o has the given opcode. +func (o Options) Has(opcode OptionCode) bool { + _, ok := o[opcode.Code()] + return ok +} - for idx, opt := range *o { - if opt.Code() == option.Code() { - (*o)[idx] = option - // Don't look further. - return - } - } - // If not found, add it. - *o = append(*o, option) +// Update updates the existing options with the passed option, adding it +// at the end if not present already +func (o Options) Update(option Option) { + o[option.Code.Code()] = option.Value.ToBytes() +} + +// ToBytes makes Options usable as an OptionValue as well. +// +// Used in the case of vendor-specific and relay agent options. +func (o Options) ToBytes() []byte { + return uio.ToBigEndian(o) } -// OptionsFromBytes parses a sequence of bytes until the end and builds a list -// of options from it. +// FromBytes parses a sequence of bytes until the end and builds a list of +// options from it. // // The sequence should not contain the DHCP magic cookie. // // Returns an error if any invalid option or length is found. -func OptionsFromBytes(data []byte) (Options, error) { - return OptionsFromBytesWithParser(data, codeGetter, ParseOption, true) +func (o Options) FromBytes(data []byte) error { + return o.fromBytesCheckEnd(data, false) } -// OptionParser is a function signature for option parsing. -type OptionParser func(code OptionCode, data []byte) (Option, error) - -// OptionCodeGetter parses a code into an OptionCode. -type OptionCodeGetter func(code uint8) OptionCode - -// codeGetter is an OptionCodeGetter for DHCP optionCodes. -func codeGetter(c uint8) OptionCode { - return optionCode(c) -} +const ( + optPad = 0 + optEnd = 255 +) -// OptionsFromBytesWithParser parses Options from byte sequences using the +// FromBytesCheckEnd parses Options from byte sequences using the // parsing function that is passed in as a paremeter -func OptionsFromBytesWithParser(data []byte, coder OptionCodeGetter, parser OptionParser, checkEndOption bool) (Options, error) { +func (o Options) fromBytesCheckEnd(data []byte, checkEndOption bool) error { if len(data) == 0 { - return nil, nil + return nil } buf := uio.NewBigEndianBuffer(data) - options := make(map[OptionCode][]byte, 10) - var order []OptionCode - // Due to RFC 2131, 3396 allowing an option to be specified multiple - // times, we have to collect all option data first, and then parse it. var end bool for buf.Len() >= 1 { // 1 byte: option code @@ -174,9 +124,9 @@ func OptionsFromBytesWithParser(data []byte, coder OptionCodeGetter, parser Opti // n bytes: data code := buf.Read8() - if code == OptionPad.Code() { + if code == optPad { continue - } else if code == OptionEnd.Code() { + } else if code == optEnd { end = true break } @@ -185,16 +135,10 @@ func OptionsFromBytesWithParser(data []byte, coder OptionCodeGetter, parser Opti // N bytes: option data data := buf.Consume(length) if data == nil { - return nil, fmt.Errorf("error collecting options: %v", buf.Error()) + return fmt.Errorf("error collecting options: %v", buf.Error()) } data = data[:length:length] - // Get the OptionCode for this guy. - c := coder(code) - if _, ok := options[c]; !ok { - order = append(order, c) - } - // RFC 2131, Section 4.1 "Options may appear only once, [...]. // The client concatenates the values of multiple instances of // the same option into a single parameter list for @@ -202,56 +146,54 @@ func OptionsFromBytesWithParser(data []byte, coder OptionCodeGetter, parser Opti // // See also RFC 3396 for concatenation order and options longer // than 255 bytes. - options[c] = append(options[c], data...) + o[code] = append(o[code], data...) } // If we never read the End option, the sender of this packet screwed // up. if !end && checkEndOption { - return nil, io.ErrUnexpectedEOF + return io.ErrUnexpectedEOF } // Any bytes left must be padding. for buf.Len() >= 1 { - if buf.Read8() != OptionPad.Code() { - return nil, ErrInvalidOptions + if buf.Read8() != optPad { + return ErrInvalidOptions } } + return nil +} - opts := make(Options, 0, len(options)) - for _, code := range order { - parsedOpt, err := parser(code, options[code]) - if err != nil { - return nil, fmt.Errorf("error parsing option code %s: %v", code, err) - } - opts = append(opts, parsedOpt) +// sortedKeys returns an ordered slice of option keys from the Options map, for +// use in serializing options to binary. +func (o Options) sortedKeys() []int { + // Send all values for a given key + var codes []int + for k := range o { + codes = append(codes, int(k)) } - return opts, nil + + sort.Sort(sort.IntSlice(codes)) + return codes } // Marshal writes options binary representations to b. func (o Options) Marshal(b *uio.Lexer) { - for _, opt := range o { - code := opt.Code().Code() - + for _, c := range o.sortedKeys() { + code := uint8(c) // Even if the End option is in there, don't marshal it until // the end. - if code == OptionEnd.Code() { - continue - } else if code == OptionPad.Code() { - // Some DHCPv4 options have fixed length and do not put - // length on the wire. - b.Write8(code) + if code == optEnd { continue } - data := opt.ToBytes() + data := o[code] // RFC 3396: If more than 256 bytes of data are given, the // option is simply listed multiple times. for len(data) > 0 { // 1 byte: option code - b.Write8(code) + b.Write8(uint8(code)) n := len(data) if n > math.MaxUint8 { @@ -267,3 +209,137 @@ func (o Options) Marshal(b *uio.Lexer) { } } } + +// String prints options using DHCP-specified option codes. +func (o Options) String() string { + return o.ToString(dhcpHumanizer) +} + +// Summary prints options in human-readable values. +// +// Summary uses vendorParser to interpret the OptionVendorSpecificInformation option. +func (o Options) Summary(vendorDecoder OptionDecoder) string { + return o.ToString(OptionHumanizer{ + ValueHumanizer: parserFor(vendorDecoder), + CodeHumanizer: func(c uint8) OptionCode { + return optionCode(c) + }, + }) +} + +// OptionParser gives a human-legible interpretation of data for the given option code. +type OptionParser func(code OptionCode, data []byte) fmt.Stringer + +// OptionHumanizer is used to interpret a set of Options for their option code +// name and values. +// +// There should be separate OptionHumanizers for each Option "space": DHCP, +// BSDP, Relay Agent Info, and others. +type OptionHumanizer struct { + ValueHumanizer OptionParser + CodeHumanizer func(code uint8) OptionCode +} + +// Stringify returns a human-readable interpretation of the option code and its +// associated data. +func (oh OptionHumanizer) Stringify(code uint8, data []byte) string { + c := oh.CodeHumanizer(code) + val := oh.ValueHumanizer(c, data) + return fmt.Sprintf("%s: %s", c, val) +} + +// dhcpHumanizer humanizes the set of DHCP option codes. +var dhcpHumanizer = OptionHumanizer{ + ValueHumanizer: parseOption, + CodeHumanizer: func(c uint8) OptionCode { + return optionCode(c) + }, +} + +// ToString uses parse to parse options into human-readable values. +func (o Options) ToString(humanizer OptionHumanizer) string { + var ret string + for _, c := range o.sortedKeys() { + code := uint8(c) + v := o[code] + optString := humanizer.Stringify(code, v) + // If this option has sub structures, offset them accordingly. + if strings.Contains(optString, "\n") { + optString = strings.Replace(optString, "\n ", "\n ", -1) + } + ret += fmt.Sprintf(" %v\n", optString) + } + return ret +} + +func parseOption(code OptionCode, data []byte) fmt.Stringer { + return parserFor(nil)(code, data) +} + +func parserFor(vendorParser OptionDecoder) OptionParser { + return func(code OptionCode, data []byte) fmt.Stringer { + return getOption(code, data, vendorParser) + } +} + +// OptionDecoder can decode a byte stream into a human-readable option. +type OptionDecoder interface { + fmt.Stringer + FromBytes([]byte) error +} + +func getOption(code OptionCode, data []byte, vendorDecoder OptionDecoder) fmt.Stringer { + var d OptionDecoder + switch code { + case OptionRouter, OptionDomainNameServer, OptionNTPServers, OptionServerIdentifier: + d = &IPs{} + + case OptionBroadcastAddress, OptionRequestedIPAddress: + d = &IP{} + + case OptionClientSystemArchitectureType: + d = &iana.Archs{} + + case OptionSubnetMask: + d = &IPMask{} + + case OptionDHCPMessageType: + var mt MessageType + d = &mt + + case OptionParameterRequestList: + d = &OptionCodeList{} + + case OptionHostName, OptionDomainName, OptionRootPath, + OptionClassIdentifier, OptionTFTPServerName, OptionBootfileName: + var s String + d = &s + + case OptionRelayAgentInformation: + d = &RelayOptions{} + + case OptionDNSDomainSearchList: + d = &rfc1035label.Labels{} + + case OptionIPAddressLeaseTime: + var dur Duration + d = &dur + + case OptionMaximumDHCPMessageSize: + var u Uint16 + d = &u + + case OptionUserClassInformation: + d = &UserClass{} + + case OptionVendorIdentifyingVendorClass: + d = &VIVCIdentifiers{} + + case OptionVendorSpecificInformation: + d = vendorDecoder + } + if d != nil && d.FromBytes(data) == nil { + return d + } + return OptionGeneric{data} +} diff --git a/dhcpv4/options_test.go b/dhcpv4/options_test.go index 0c1c1fa..6c5393c 100644 --- a/dhcpv4/options_test.go +++ b/dhcpv4/options_test.go @@ -11,154 +11,148 @@ import ( ) func TestParseOption(t *testing.T) { - // Generic - option := []byte{192, 168, 1, 254} // Name server option - opt, err := ParseOption(OptionNameServer, option) - require.NoError(t, err) - generic := opt.(*OptionGeneric) - require.Equal(t, OptionNameServer, generic.Code()) - require.Equal(t, []byte{192, 168, 1, 254}, generic.Data) - require.Equal(t, "Name Server -> [192 168 1 254]", generic.String()) - - // Option subnet mask - option = []byte{255, 255, 255, 0} - opt, err = ParseOption(OptionSubnetMask, option) - require.NoError(t, err) - require.Equal(t, OptionSubnetMask, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") - - // Option router - option = []byte{192, 168, 1, 1} - opt, err = ParseOption(OptionRouter, option) - require.NoError(t, err) - require.Equal(t, OptionRouter, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") - - // Option domain name server - option = []byte{192, 168, 1, 1} - opt, err = ParseOption(OptionDomainNameServer, option) - require.NoError(t, err) - require.Equal(t, OptionDomainNameServer, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") - - // Option host name - option = []byte{'t', 'e', 's', 't'} - opt, err = ParseOption(OptionHostName, option) - require.NoError(t, err) - require.Equal(t, OptionHostName, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") - - // Option domain name - option = []byte{'t', 'e', 's', 't'} - opt, err = ParseOption(OptionDomainName, option) - require.NoError(t, err) - require.Equal(t, OptionDomainName, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") - - // Option root path - option = []byte{'/', 'f', 'o', 'o'} - opt, err = ParseOption(OptionRootPath, option) - require.NoError(t, err) - require.Equal(t, OptionRootPath, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") - - // Option broadcast address - option = []byte{255, 255, 255, 255} - opt, err = ParseOption(OptionBroadcastAddress, option) - require.NoError(t, err) - require.Equal(t, OptionBroadcastAddress, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") - - // Option NTP servers - option = []byte{10, 10, 10, 10} - opt, err = ParseOption(OptionNTPServers, option) - require.NoError(t, err) - require.Equal(t, OptionNTPServers, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") - - // Requested IP address - option = []byte{1, 2, 3, 4} - opt, err = ParseOption(OptionRequestedIPAddress, option) - require.NoError(t, err) - require.Equal(t, OptionRequestedIPAddress, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") - - // Requested IP address lease time - option = []byte{0, 0, 0, 0} - opt, err = ParseOption(OptionIPAddressLeaseTime, option) - require.NoError(t, err) - require.Equal(t, OptionIPAddressLeaseTime, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") - - // Message type - option = []byte{1} - opt, err = ParseOption(OptionDHCPMessageType, option) - require.NoError(t, err) - require.Equal(t, OptionDHCPMessageType, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") - - // Option server ID - option = []byte{1, 2, 3, 4} - opt, err = ParseOption(OptionServerIdentifier, option) - require.NoError(t, err) - require.Equal(t, OptionServerIdentifier, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") - - // Parameter request list - option = []byte{5, 53, 61} - opt, err = ParseOption(OptionParameterRequestList, option) - require.NoError(t, err) - require.Equal(t, OptionParameterRequestList, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") - - // Option max message size - option = []byte{1, 2} - opt, err = ParseOption(OptionMaximumDHCPMessageSize, option) - require.NoError(t, err) - require.Equal(t, OptionMaximumDHCPMessageSize, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") - - // Option class identifier - option = []byte{'t', 'e', 's', 't'} - opt, err = ParseOption(OptionClassIdentifier, option) - require.NoError(t, err) - require.Equal(t, OptionClassIdentifier, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") - - // Option TFTP server name - option = []byte{'t', 'e', 's', 't'} - opt, err = ParseOption(OptionTFTPServerName, option) - require.NoError(t, err) - require.Equal(t, OptionTFTPServerName, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") - - // Option Bootfile name - option = []byte{'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't'} - opt, err = ParseOption(OptionBootfileName, option) - require.NoError(t, err) - require.Equal(t, OptionBootfileName, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") + for _, tt := range []struct { + code OptionCode + value []byte + want string + }{ + { + code: OptionNameServer, + value: []byte{192, 168, 1, 254}, + want: "[192 168 1 254]", + }, + { + code: OptionSubnetMask, + value: []byte{255, 255, 255, 0}, + want: "ffffff00", + }, + { + code: OptionRouter, + value: []byte{192, 168, 1, 1, 192, 168, 2, 1}, + want: "192.168.1.1, 192.168.2.1", + }, + { + code: OptionDomainNameServer, + value: []byte{192, 168, 1, 1, 192, 168, 2, 1}, + want: "192.168.1.1, 192.168.2.1", + }, + { + code: OptionNTPServers, + value: []byte{192, 168, 1, 1, 192, 168, 2, 1}, + want: "192.168.1.1, 192.168.2.1", + }, + { + code: OptionServerIdentifier, + value: []byte{192, 168, 1, 1, 192, 168, 2, 1}, + want: "192.168.1.1, 192.168.2.1", + }, + { + code: OptionHostName, + value: []byte("test"), + want: "test", + }, + { + code: OptionDomainName, + value: []byte("test"), + want: "test", + }, + { + code: OptionRootPath, + value: []byte("test"), + want: "test", + }, + { + code: OptionClassIdentifier, + value: []byte("test"), + want: "test", + }, + { + code: OptionTFTPServerName, + value: []byte("test"), + want: "test", + }, + { + code: OptionBootfileName, + value: []byte("test"), + want: "test", + }, + { + code: OptionBroadcastAddress, + value: []byte{192, 168, 1, 1}, + want: "192.168.1.1", + }, + { + code: OptionRequestedIPAddress, + value: []byte{192, 168, 1, 1}, + want: "192.168.1.1", + }, + { + code: OptionIPAddressLeaseTime, + value: []byte{0, 0, 0, 12}, + want: "12s", + }, + { + code: OptionDHCPMessageType, + value: []byte{1}, + want: "DISCOVER", + }, + { + code: OptionParameterRequestList, + value: []byte{3, 4, 5}, + want: "Router, Time Server, Name Server", + }, + { + code: OptionMaximumDHCPMessageSize, + value: []byte{1, 2}, + want: "258", + }, + { + code: OptionUserClassInformation, + value: []byte{4, 't', 'e', 's', 't', 3, 'f', 'o', 'o'}, + want: "test, foo", + }, + { + code: OptionRelayAgentInformation, + value: []byte{1, 4, 129, 168, 0, 1}, + want: " unknown (1): [129 168 0 1]\n", + }, + { + code: OptionClientSystemArchitectureType, + value: []byte{0, 0}, + want: "Intel x86PC", + }, + } { + s := parseOption(tt.code, tt.value) + if got := s.String(); got != tt.want { + t.Errorf("parseOption(%s, %v) = %s, want %s", tt.code, tt.value, got, tt.want) + } + } +} - // Option user class information - option = []byte{4, 't', 'e', 's', 't'} - opt, err = ParseOption(OptionUserClassInformation, option) - require.NoError(t, err) - require.Equal(t, OptionUserClassInformation, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") +func TestOptionToBytes(t *testing.T) { + o := Option{ + Code: OptionDHCPMessageType, + Value: &OptionGeneric{[]byte{byte(MessageTypeDiscover)}}, + } + serialized := o.Value.ToBytes() + expected := []byte{1} + require.Equal(t, expected, serialized) +} - // Option relay agent information - option = []byte{1, 4, 129, 168, 0, 1} - opt, err = ParseOption(OptionRelayAgentInformation, option) - require.NoError(t, err) - require.Equal(t, OptionRelayAgentInformation, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") +func TestOptionString(t *testing.T) { + o := Option{ + Code: OptionDHCPMessageType, + Value: MessageTypeDiscover, + } + require.Equal(t, "DHCP Message Type: DISCOVER", o.String()) +} - // Option client system architecture type option - option = []byte{'t', 'e', 's', 't'} - opt, err = ParseOption(OptionClientSystemArchitectureType, option) - require.NoError(t, err) - require.Equal(t, OptionClientSystemArchitectureType, opt.Code(), "Code") - require.Equal(t, option, opt.ToBytes(), "ToBytes") +func TestOptionStringUnknown(t *testing.T) { + o := Option{ + Code: GenericOptionCode(102), // Returend option code. + Value: &OptionGeneric{[]byte{byte(MessageTypeDiscover)}}, + } + require.Equal(t, "unknown (102): [1]", o.String()) } func TestOptionsMarshal(t *testing.T) { @@ -172,10 +166,7 @@ func TestOptionsMarshal(t *testing.T) { }, { opts: Options{ - &OptionGeneric{ - OptionCode: optionCode(5), - Data: []byte{1, 2, 3, 4}, - }, + 5: []byte{1, 2, 3, 4}, }, want: []byte{ 5 /* key */, 4 /* length */, 1, 2, 3, 4, @@ -184,14 +175,9 @@ func TestOptionsMarshal(t *testing.T) { { // Test sorted key order. opts: Options{ - &OptionGeneric{ - OptionCode: optionCode(5), - Data: []byte{1, 2, 3}, - }, - &OptionGeneric{ - OptionCode: optionCode(100), - Data: []byte{101, 102, 103}, - }, + 5: []byte{1, 2, 3}, + 100: []byte{101, 102, 103}, + 255: []byte{}, }, want: []byte{ 5, 3, 1, 2, 3, @@ -201,10 +187,7 @@ func TestOptionsMarshal(t *testing.T) { { // Test RFC 3396. opts: Options{ - &OptionGeneric{ - OptionCode: optionCode(5), - Data: bytes.Repeat([]byte{10}, math.MaxUint8+1), - }, + 5: bytes.Repeat([]byte{10}, math.MaxUint8+1), }, want: append(append( []byte{5, math.MaxUint8}, bytes.Repeat([]byte{10}, math.MaxUint8)...), @@ -262,10 +245,7 @@ func TestOptionsUnmarshal(t *testing.T) { byte(OptionEnd), }, want: Options{ - &OptionGeneric{ - OptionCode: optionCode(3), - Data: []byte{5, 6}, - }, + 3: []byte{5, 6}, }, }, { @@ -276,10 +256,7 @@ func TestOptionsUnmarshal(t *testing.T) { byte(OptionEnd), ), want: Options{ - &OptionGeneric{ - OptionCode: optionCode(3), - Data: bytes.Repeat([]byte{10}, math.MaxUint8+5), - }, + 3: bytes.Repeat([]byte{10}, math.MaxUint8+5), }, }, { @@ -289,14 +266,8 @@ func TestOptionsUnmarshal(t *testing.T) { byte(OptionEnd), }, want: Options{ - &OptionGeneric{ - OptionCode: optionCode(10), - Data: []byte{255, 254}, - }, - &OptionGeneric{ - OptionCode: optionCode(11), - Data: []byte{5, 5, 5}, - }, + 10: []byte{255, 254}, + 11: []byte{5, 5, 5}, }, }, { @@ -305,15 +276,13 @@ func TestOptionsUnmarshal(t *testing.T) { byte(OptionEnd), ), want: Options{ - &OptionGeneric{ - OptionCode: optionCode(10), - Data: []byte{255, 254}, - }, + 10: []byte{255, 254}, }, }, } { t.Run(fmt.Sprintf("Test %02d", i), func(t *testing.T) { - opt, err := OptionsFromBytesWithParser(tt.input, codeGetter, ParseOptionGeneric, true) + opt := make(Options) + err := opt.fromBytesCheckEnd(tt.input, true) if tt.wantError { require.Error(t, err) } else { diff --git a/dhcpv4/server_test.go b/dhcpv4/server_test.go index 4307924..8626451 100644 --- a/dhcpv4/server_test.go +++ b/dhcpv4/server_test.go @@ -41,19 +41,15 @@ func DORAHandler(conn net.PacketConn, peer net.Addr, m *DHCPv4) { log.Printf("NewReplyFromRequest failed: %v", err) return } - reply.UpdateOption(&OptServerIdentifier{ServerID: net.IP{1, 2, 3, 4}}) - opt := m.GetOneOption(OptionDHCPMessageType) - if opt == nil { - log.Printf("No message type found!") - return - } - switch opt.(*OptMessageType).MessageType { + reply.UpdateOption(OptServerIdentifier(net.IP{1, 2, 3, 4})) + mt := GetMessageType(m.Options) + switch mt { case MessageTypeDiscover: - reply.UpdateOption(&OptMessageType{MessageType: MessageTypeOffer}) + reply.UpdateOption(OptMessageType(MessageTypeOffer)) case MessageTypeRequest: - reply.UpdateOption(&OptMessageType{MessageType: MessageTypeAck}) + reply.UpdateOption(OptMessageType(MessageTypeAck)) default: - log.Printf("Unhandled message type: %v", opt.(*OptMessageType).MessageType) + log.Printf("Unhandled message type: %v", mt) return } diff --git a/dhcpv4/types.go b/dhcpv4/types.go index 6214dbd..e1762cc 100644 --- a/dhcpv4/types.go +++ b/dhcpv4/types.go @@ -2,6 +2,8 @@ package dhcpv4 import ( "fmt" + + "github.com/u-root/u-root/pkg/uio" ) // values from http://www.networksorcery.com/enp/protocol/dhcp.htm and @@ -36,6 +38,13 @@ const ( MessageTypeInform MessageType = 8 ) +// ToBytes returns the serialized version of this option described by RFC 2132, +// Section 9.6. +func (m MessageType) ToBytes() []byte { + return []byte{byte(m)} +} + +// String prints a human-readable message type name. func (m MessageType) String() string { if s, ok := messageTypeToString[m]; ok { return s @@ -43,6 +52,14 @@ func (m MessageType) String() string { return fmt.Sprintf("unknown (%d)", byte(m)) } +// FromBytes reads a message type from data as described by RFC 2132, Section +// 9.6. +func (m *MessageType) FromBytes(data []byte) error { + buf := uio.NewBigEndianBuffer(data) + *m = MessageType(buf.Read8()) + return buf.FinError() +} + var messageTypeToString = map[MessageType]string{ MessageTypeDiscover: "DISCOVER", MessageTypeOffer: "OFFER", @@ -81,22 +98,40 @@ var opcodeToString = map[OpcodeType]string{ // with the same Code value, as vendor-specific options use option codes that // have the same value, but mean a different thing. type OptionCode interface { + // Code is the 1 byte option code for the wire. Code() uint8 + + // String returns the option's name. String() string } // optionCode is a DHCP option code. type optionCode uint8 +// Code implements OptionCode.Code. func (o optionCode) Code() uint8 { return uint8(o) } +// String returns an option name. func (o optionCode) String() string { if s, ok := optionCodeToString[o]; ok { return s } - return fmt.Sprintf("unknown (%d)", o) + return fmt.Sprintf("unknown (%d)", uint8(o)) +} + +// GenericOptionCode is an unnamed option code. +type GenericOptionCode uint8 + +// Code implements OptionCode.Code. +func (o GenericOptionCode) Code() uint8 { + return uint8(o) +} + +// String returns the option's name. +func (o GenericOptionCode) String() string { + return fmt.Sprintf("unknown (%d)", uint8(o)) } // DHCPv4 Options @@ -263,7 +298,7 @@ const ( OptionEnd optionCode = 255 ) -var optionCodeToString = map[optionCode]string{ +var optionCodeToString = map[OptionCode]string{ OptionPad: "Pad", OptionSubnetMask: "Subnet Mask", OptionTimeOffset: "Time Offset", diff --git a/dhcpv4/ztpv4/ztp.go b/dhcpv4/ztpv4/ztp.go index 18075e9..4401e9d 100644 --- a/dhcpv4/ztpv4/ztp.go +++ b/dhcpv4/ztpv4/ztp.go @@ -18,11 +18,10 @@ var errVendorOptionMalformed = errors.New("malformed vendor option") // ParseVendorData will try to parse dhcp4 options looking for more // specific vendor data (like model, serial number, etc). func ParseVendorData(packet *dhcpv4.DHCPv4) (*VendorData, error) { - opt := packet.GetOneOption(dhcpv4.OptionClassIdentifier) - if opt == nil { + vc := dhcpv4.GetClassIdentifier(packet.Options) + if len(vc) == 0 { return nil, errors.New("vendor options not found") } - vc := opt.(*dhcpv4.OptClassIdentifier).Identifier vd := &VendorData{} switch { @@ -59,9 +58,8 @@ func ParseVendorData(packet *dhcpv4.DHCPv4) (*VendorData, error) { p := strings.Split(vc, "-") if len(p) < 3 { vd.Model = p[1] - if opt := packet.GetOneOption(dhcpv4.OptionHostName); opt != nil { - vd.Serial = opt.(*dhcpv4.OptHostName).HostName - } else { + vd.Serial = dhcpv4.GetHostName(packet.Options) + if len(vd.Serial) == 0 { return nil, errors.New("host name option is missing") } } else { diff --git a/dhcpv4/ztpv4/ztp_test.go b/dhcpv4/ztpv4/ztp_test.go index 680f15a..6e050d9 100644 --- a/dhcpv4/ztpv4/ztp_test.go +++ b/dhcpv4/ztpv4/ztp_test.go @@ -54,15 +54,10 @@ func TestParseV4VendorClass(t *testing.T) { } if tc.vc != "" { - packet.UpdateOption(&dhcpv4.OptClassIdentifier{ - Identifier: tc.vc, - }) + packet.UpdateOption(dhcpv4.OptClassIdentifier(tc.vc)) } - if tc.hostname != "" { - packet.UpdateOption(&dhcpv4.OptHostName{ - HostName: tc.hostname, - }) + packet.UpdateOption(dhcpv4.OptHostName(tc.hostname)) } vd, err := ParseVendorData(packet) |