diff options
-rw-r--r-- | dhcpv4/bsdp/boot_image.go | 113 | ||||
-rw-r--r-- | dhcpv4/bsdp/boot_image_test.go | 130 | ||||
-rw-r--r-- | dhcpv4/bsdp/bsdp.go | 116 | ||||
-rw-r--r-- | dhcpv4/bsdp/bsdp_option_message_type.go | 75 | ||||
-rw-r--r-- | dhcpv4/bsdp/bsdp_option_message_type_test.go | 48 | ||||
-rw-r--r-- | dhcpv4/bsdp/bsdp_option_reply_port.go | 60 | ||||
-rw-r--r-- | dhcpv4/bsdp/bsdp_option_reply_port_test.go | 44 | ||||
-rw-r--r-- | dhcpv4/bsdp/bsdp_option_selected_boot_image_id.go | 59 | ||||
-rw-r--r-- | dhcpv4/bsdp/bsdp_option_selected_boot_image_id_test.go | 56 | ||||
-rw-r--r-- | dhcpv4/bsdp/bsdp_option_version.go | 59 | ||||
-rw-r--r-- | dhcpv4/bsdp/bsdp_option_version_test.go | 44 | ||||
-rw-r--r-- | dhcpv4/bsdp/bsdp_test.go | 121 | ||||
-rw-r--r-- | dhcpv4/bsdp/types.go | 28 |
13 files changed, 699 insertions, 254 deletions
diff --git a/dhcpv4/bsdp/boot_image.go b/dhcpv4/bsdp/boot_image.go new file mode 100644 index 0000000..954432e --- /dev/null +++ b/dhcpv4/bsdp/boot_image.go @@ -0,0 +1,113 @@ +// +build darwin + +package bsdp + +import ( + "encoding/binary" + "fmt" +) + +// BootImageType represents the different BSDP boot image types. +type BootImageType byte + +// Different types of BootImages - e.g. for different flavors of macOS. +const ( + BootImageTypeMacOS9 BootImageType = 0 + BootImageTypeMacOSX BootImageType = 1 + BootImageTypeMacOSXServer BootImageType = 2 + BootImageTypeHardwareDiagnostics BootImageType = 3 + // 4 - 127 are reserved for future use. +) + +// BootImageTypeToString maps the different BootImageTypes to human-readable +// representations. +var BootImageTypeToString = map[BootImageType]string{ + BootImageTypeMacOS9: "macOS 9", + BootImageTypeMacOSX: "macOS", + BootImageTypeMacOSXServer: "macOS Server", + BootImageTypeHardwareDiagnostics: "Hardware Diagnostic", +} + +// BootImageID describes a boot image ID - whether it's an install image and +// what kind of boot image (e.g. OS 9, macOS, hardware diagnostics) +type BootImageID struct { + IsInstall bool + ImageType BootImageType + Index uint16 +} + +// ToBytes serializes a BootImageID to network-order bytes. +func (b BootImageID) ToBytes() []byte { + bytes := make([]byte, 4) + if b.IsInstall { + bytes[0] |= 0x80 + } + bytes[0] |= byte(b.ImageType) + binary.BigEndian.PutUint16(bytes[2:], b.Index) + return bytes +} + +// String converts a BootImageID to a human-readable representation. +func (b BootImageID) String() string { + s := fmt.Sprintf("[%d]", b.Index) + if b.IsInstall { + s += " installable" + } else { + s += " uninstallable" + } + t, ok := BootImageTypeToString[b.ImageType] + if !ok { + t = "unknown" + } + return s + " " + t + " image" +} + +// BootImageIDFromBytes deserializes a collection of 4 bytes to a BootImageID. +func BootImageIDFromBytes(bytes []byte) (*BootImageID, error) { + if len(bytes) < 4 { + return nil, fmt.Errorf("not enough bytes to serialize BootImageID") + } + return &BootImageID{ + IsInstall: bytes[0]&0x80 != 0, + ImageType: BootImageType(bytes[0] & 0x7f), + Index: binary.BigEndian.Uint16(bytes[2:]), + }, nil +} + +// BootImage describes a boot image - contains the boot image ID and the name. +type BootImage struct { + ID BootImageID + Name string +} + +// ToBytes converts a BootImage to a slice of bytes. +func (b *BootImage) ToBytes() []byte { + bytes := b.ID.ToBytes() + bytes = append(bytes, byte(len(b.Name))) + bytes = append(bytes, []byte(b.Name)...) + return bytes +} + +// String converts a BootImage to a human-readable representation. +func (b *BootImage) String() string { + return fmt.Sprintf("%v %v", b.Name, b.ID.String()) +} + +// BootImageFromBytes returns a deserialized BootImage struct from bytes. +func BootImageFromBytes(bytes []byte) (*BootImage, error) { + // Should at least contain 4 bytes of BootImageID + byte for length of + // boot image name. + if len(bytes) < 5 { + return nil, fmt.Errorf("not enough bytes to serialize BootImage") + } + imageID, err := BootImageIDFromBytes(bytes[:4]) + if err != nil { + return nil, err + } + nameLength := int(bytes[4]) + if 5+nameLength > len(bytes) { + return nil, fmt.Errorf("not enough bytes for BootImage") + } + name := string(bytes[5 : 5+nameLength]) + return &BootImage{ID: *imageID, Name: name}, nil +} diff --git a/dhcpv4/bsdp/boot_image_test.go b/dhcpv4/bsdp/boot_image_test.go new file mode 100644 index 0000000..ebf02c3 --- /dev/null +++ b/dhcpv4/bsdp/boot_image_test.go @@ -0,0 +1,130 @@ +// +build darwin + +package bsdp + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +/* + * BootImageID + */ +func TestBootImageIDToBytes(t *testing.T) { + b := BootImageID{ + IsInstall: true, + ImageType: BootImageTypeMacOSX, + Index: 0x1000, + } + actual := b.ToBytes() + expected := []byte{0x81, 0, 0x10, 0} + require.Equal(t, expected, actual) + + b.IsInstall = false + actual = b.ToBytes() + expected = []byte{0x01, 0, 0x10, 0} + require.Equal(t, expected, actual) +} + +func TestBootImageIDFromBytes(t *testing.T) { + b := BootImageID{ + IsInstall: false, + ImageType: BootImageTypeMacOSX, + Index: 0x1000, + } + newBootImage, err := BootImageIDFromBytes(b.ToBytes()) + require.NoError(t, err) + require.Equal(t, b, *newBootImage) + + b = BootImageID{ + IsInstall: true, + ImageType: BootImageTypeMacOSX, + Index: 0x1011, + } + newBootImage, err = BootImageIDFromBytes(b.ToBytes()) + require.NoError(t, err) + require.Equal(t, b, *newBootImage) +} + +func TestBootImageIDFromBytesFail(t *testing.T) { + serialized := []byte{0x81, 0, 0x10} // intentionally left short + deserialized, err := BootImageIDFromBytes(serialized) + require.Nil(t, deserialized) + require.Error(t, err) +} + +/* + * BootImage + */ +func TestBootImageToBytes(t *testing.T) { + b := BootImage{ + ID: BootImageID{ + IsInstall: true, + ImageType: BootImageTypeMacOSX, + Index: 0x1000, + }, + Name: "bsdp-1", + } + expected := []byte{ + 0x81, 0, 0x10, 0, // boot image ID + 6, // len(Name) + 98, 115, 100, 112, 45, 49, // byte-encoding of Name + } + actual := b.ToBytes() + require.Equal(t, expected, actual) + + b = BootImage{ + ID: BootImageID{ + IsInstall: false, + ImageType: BootImageTypeMacOSX, + Index: 0x1010, + }, + Name: "bsdp-21", + } + expected = []byte{ + 0x1, 0, 0x10, 0x10, // boot image ID + 7, // len(Name) + 98, 115, 100, 112, 45, 50, 49, // byte-encoding of Name + } + actual = b.ToBytes() + require.Equal(t, expected, actual) +} + +func TestBootImageFromBytes(t *testing.T) { + input := []byte{ + 0x1, 0, 0x10, 0x10, // boot image ID + 7, // len(Name) + 98, 115, 100, 112, 45, 50, 49, // byte-encoding of Name + } + b, err := BootImageFromBytes(input) + require.NoError(t, err) + expectedBootImage := BootImage{ + ID: BootImageID{ + IsInstall: false, + ImageType: BootImageTypeMacOSX, + Index: 0x1010, + }, + Name: "bsdp-21", + } + require.Equal(t, expectedBootImage, *b) +} + +func TestBootImageFromBytesOnlyBootImageID(t *testing.T) { + // Only a BootImageID, nothing else. + input := []byte{0x1, 0, 0x10, 0x10} + b, err := BootImageFromBytes(input) + require.Nil(t, b) + require.Error(t, err) +} + +func TestBootImageFromBytesShortBootImage(t *testing.T) { + input := []byte{ + 0x1, 0, 0x10, 0x10, // boot image ID + 7, // len(Name) + 98, 115, 100, 112, 45, 50, // Name bytes (intentionally off-by-one) + } + b, err := BootImageFromBytes(input) + require.Nil(t, b) + require.Error(t, err) +} diff --git a/dhcpv4/bsdp/bsdp.go b/dhcpv4/bsdp/bsdp.go index e93b654..1c2b100 100644 --- a/dhcpv4/bsdp/bsdp.go +++ b/dhcpv4/bsdp/bsdp.go @@ -7,7 +7,6 @@ package bsdp // http://opensource.apple.com/source/bootp/bootp-198.1/Documentation/BSDP.doc import ( - "encoding/binary" "errors" "fmt" "log" @@ -23,70 +22,6 @@ import ( // prefer this BSDP-specific option over the DHCP standard option. const MaxDHCPMessageSize = 1500 -// BootImageID describes a boot image ID - whether it's an install image and -// what kind of boot image (e.g. OS 9, macOS, hardware diagnostics) -type BootImageID struct { - IsInstall bool - ImageType BootImageType - Index uint16 -} - -// ToBytes serializes a BootImageID to network-order bytes. -func (b BootImageID) ToBytes() []byte { - bytes := make([]byte, 4) - if b.IsInstall { - bytes[0] |= 0x80 - } - bytes[0] |= byte(b.ImageType) - binary.BigEndian.PutUint16(bytes[2:], b.Index) - return bytes -} - -// BootImageIDFromBytes deserializes a collection of 4 bytes to a BootImageID. -func BootImageIDFromBytes(bytes []byte) (*BootImageID, error) { - if len(bytes) < 4 { - return nil, fmt.Errorf("not enough bytes to serialize BootImageID") - } - return &BootImageID{ - IsInstall: bytes[0]&0x80 != 0, - ImageType: BootImageType(bytes[0] & 0x7f), - Index: binary.BigEndian.Uint16(bytes[2:]), - }, nil -} - -// BootImage describes a boot image - contains the boot image ID and the name. -type BootImage struct { - ID BootImageID - Name string -} - -// ToBytes converts a BootImage to a slice of bytes. -func (b *BootImage) ToBytes() []byte { - bytes := b.ID.ToBytes() - bytes = append(bytes, byte(len(b.Name))) - bytes = append(bytes, []byte(b.Name)...) - return bytes -} - -// BootImageFromBytes returns a deserialized BootImage struct from bytes. -func BootImageFromBytes(bytes []byte) (*BootImage, error) { - // Should at least contain 4 bytes of BootImageID + byte for length of - // boot image name. - if len(bytes) < 5 { - return nil, fmt.Errorf("not enough bytes to serialize BootImage") - } - imageID, err := BootImageIDFromBytes(bytes[:4]) - if err != nil { - return nil, err - } - nameLength := int(bytes[4]) - if 5+nameLength > len(bytes) { - return nil, fmt.Errorf("not enough bytes for BootImage") - } - name := string(bytes[5 : 5+nameLength]) - return &BootImage{ID: *imageID, Name: name}, nil -} - // makeVendorClassIdentifier calls the sysctl syscall on macOS to get the // platform model. func makeVendorClassIdentifier() (string, error) { @@ -170,12 +105,6 @@ func needsReplyPort(replyPort uint16) bool { return replyPort != 0 && replyPort != dhcpv4.ClientPort } -func serializeReplyPort(replyPort uint16) []byte { - bytes := make([]byte, 2) - binary.BigEndian.PutUint16(bytes, replyPort) - return bytes -} - // NewInformListForInterface creates a new INFORM packet for interface ifname // with configuration options specified by config. func NewInformListForInterface(iface string, replyPort uint16) (*dhcpv4.DHCPv4, error) { @@ -191,23 +120,12 @@ func NewInformListForInterface(iface string, replyPort uint16) (*dhcpv4.DHCPv4, // These are vendor-specific options used to pass along BSDP information. vendorOpts := []dhcpv4.Option{ - dhcpv4.OptionGeneric{ - OptionCode: OptionMessageType, - Data: []byte{byte(MessageTypeList)}, - }, - dhcpv4.OptionGeneric{ - OptionCode: OptionVersion, - Data: Version1_1, - }, + &OptMessageType{MessageTypeList}, + &OptVersion{Version1_1}, } if needsReplyPort(replyPort) { - vendorOpts = append(vendorOpts, - dhcpv4.OptionGeneric{ - OptionCode: OptionReplyPort, - Data: serializeReplyPort(replyPort), - }, - ) + vendorOpts = append(vendorOpts, &OptReplyPort{replyPort}) } var vendorOptsBytes []byte for _, opt := range vendorOpts { @@ -230,7 +148,7 @@ func NewInformListForInterface(iface string, replyPort uint16) (*dhcpv4.DHCPv4, if err != nil { return nil, err } - d.AddOption(&dhcpv4.OptClassIdentifier{Identifier: vendorClassID}) + d.AddOption(&dhcpv4.OptClassIdentifier{vendorClassID}) d.AddOption(&dhcpv4.OptionGeneric{OptionCode: dhcpv4.OptionEnd}) return d, nil } @@ -260,18 +178,9 @@ func InformSelectForAck(ack dhcpv4.DHCPv4, replyPort uint16, selectedImage BootI // Data for OptionSelectedBootImageID vendorOpts := []dhcpv4.Option{ - dhcpv4.OptionGeneric{ - OptionCode: OptionMessageType, - Data: []byte{byte(MessageTypeSelect)}, - }, - dhcpv4.OptionGeneric{ - OptionCode: OptionVersion, - Data: Version1_1, - }, - dhcpv4.OptionGeneric{ - OptionCode: OptionSelectedBootImageID, - Data: selectedImage.ID.ToBytes(), - }, + &OptMessageType{MessageTypeSelect}, + &OptVersion{Version1_1}, + &OptSelectedBootImageID{selectedImage.ID}, } // Find server IP address @@ -289,19 +198,16 @@ func InformSelectForAck(ack dhcpv4.DHCPv4, replyPort uint16, selectedImage BootI // Validate replyPort if requested. if needsReplyPort(replyPort) { - vendorOpts = append(vendorOpts, dhcpv4.OptionGeneric{ - OptionCode: OptionReplyPort, - Data: serializeReplyPort(replyPort), - }) + vendorOpts = append(vendorOpts, &OptReplyPort{replyPort}) } vendorClassID, err := makeVendorClassIdentifier() if err != nil { return nil, err } - d.AddOption(&dhcpv4.OptClassIdentifier{Identifier: vendorClassID}) + d.AddOption(&dhcpv4.OptClassIdentifier{vendorClassID}) d.AddOption(&dhcpv4.OptParameterRequestList{ - RequestedOpts: []dhcpv4.OptionCode{ + []dhcpv4.OptionCode{ dhcpv4.OptionSubnetMask, dhcpv4.OptionRouter, dhcpv4.OptionBootfileName, @@ -309,7 +215,7 @@ func InformSelectForAck(ack dhcpv4.DHCPv4, replyPort uint16, selectedImage BootI dhcpv4.OptionClassIdentifier, }, }) - d.AddOption(&dhcpv4.OptMessageType{MessageType: dhcpv4.MessageTypeInform}) + d.AddOption(&dhcpv4.OptMessageType{dhcpv4.MessageTypeInform}) var vendorOptsBytes []byte for _, opt := range vendorOpts { vendorOptsBytes = append(vendorOptsBytes, opt.ToBytes()...) diff --git a/dhcpv4/bsdp/bsdp_option_message_type.go b/dhcpv4/bsdp/bsdp_option_message_type.go new file mode 100644 index 0000000..ad80fc1 --- /dev/null +++ b/dhcpv4/bsdp/bsdp_option_message_type.go @@ -0,0 +1,75 @@ +// +build darwin + +package bsdp + +import ( + "fmt" + + "github.com/insomniacslk/dhcp/dhcpv4" +) + +// Implements the BSDP option message type. Can be one of LIST, SELECT, or +// FAILED. + +// MessageType represents the different BSDP message types. +type MessageType byte + +// BSDP Message types - e.g. LIST, SELECT, FAILED +const ( + MessageTypeList MessageType = 1 + MessageTypeSelect MessageType = 2 + MessageTypeFailed MessageType = 3 +) + +// MessageTypeToString maps each BSDP message type to a human-readable string. +var MessageTypeToString = map[MessageType]string{ + MessageTypeList: "LIST", + MessageTypeSelect: "SELECT", + 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) { + if len(data) < 3 { + return nil, dhcpv4.ErrShortByteStream + } + code := dhcpv4.OptionCode(data[0]) + if code != OptionMessageType { + return nil, fmt.Errorf("expected option %v, got %v instead", OptionMessageType, code) + } + length := int(data[1]) + if length != 1 { + return nil, fmt.Errorf("expected length 1, got %d instead", length) + } + return &OptMessageType{Type: MessageType(data[2])}, nil +} + +// 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.Code()), 1, byte(o.Type)} +} + +// String returns a human-readable string for this option. +func (o *OptMessageType) String() string { + s, ok := MessageTypeToString[o.Type] + if !ok { + s = "UNKNOWN" + } + return fmt.Sprintf("BSDP Message Type -> %s", s) +} + +// Length returns the length of the data portion of this option. +func (o *OptMessageType) Length() int { + return 1 +} diff --git a/dhcpv4/bsdp/bsdp_option_message_type_test.go b/dhcpv4/bsdp/bsdp_option_message_type_test.go new file mode 100644 index 0000000..f74644c --- /dev/null +++ b/dhcpv4/bsdp/bsdp_option_message_type_test.go @@ -0,0 +1,48 @@ +// +build darwin + +package bsdp + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOptMessageTypeInterfaceMethods(t *testing.T) { + o := OptMessageType{MessageTypeList} + require.Equal(t, OptionMessageType, o.Code(), "Code") + require.Equal(t, 1, o.Length(), "Length") + require.Equal(t, []byte{1, 1, 1}, o.ToBytes(), "ToBytes") +} + +func TestParseOptMessageType(t *testing.T) { + data := []byte{1, 1, 1} // DISCOVER + o, err := ParseOptMessageType(data) + require.NoError(t, err) + require.Equal(t, &OptMessageType{MessageTypeList}, o) + + // Short byte stream + data = []byte{1, 1} + _, err = ParseOptMessageType(data) + require.Error(t, err, "should get error from short byte stream") + + // Wrong code + data = []byte{54, 1, 1} + _, err = ParseOptMessageType(data) + require.Error(t, err, "should get error from wrong code") + + // Bad length + data = []byte{1, 5, 1} + _, err = ParseOptMessageType(data) + require.Error(t, err, "should get error from bad length") +} + +func TestOptMessageTypeString(t *testing.T) { + // known + 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()) +} diff --git a/dhcpv4/bsdp/bsdp_option_reply_port.go b/dhcpv4/bsdp/bsdp_option_reply_port.go new file mode 100644 index 0000000..5e11058 --- /dev/null +++ b/dhcpv4/bsdp/bsdp_option_reply_port.go @@ -0,0 +1,60 @@ +// +build darwin + +package bsdp + +import ( + "encoding/binary" + "fmt" + + "github.com/insomniacslk/dhcp/dhcpv4" +) + +// 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. + +// OptReplyPort represents a BSDP protocol version. +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) { + if len(data) < 4 { + return nil, dhcpv4.ErrShortByteStream + } + code := dhcpv4.OptionCode(data[0]) + if code != OptionReplyPort { + return nil, fmt.Errorf("expected option %v, got %v instead", OptionReplyPort, code) + } + length := int(data[1]) + if length != 2 { + return nil, fmt.Errorf("expected length 2, got %d instead", length) + } + port := binary.BigEndian.Uint16(data[2:4]) + return &OptReplyPort{port}, nil +} + +// 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 { + serialized := make([]byte, 2) + binary.BigEndian.PutUint16(serialized, o.Port) + return append([]byte{byte(o.Code()), 2}, serialized...) +} + +// String returns a human-readable string for this option. +func (o *OptReplyPort) String() string { + return fmt.Sprintf("BSDP Reply Port -> %v", o.Port) +} + +// Length returns the length of the data portion of this option. +func (o *OptReplyPort) Length() int { + return 2 +} diff --git a/dhcpv4/bsdp/bsdp_option_reply_port_test.go b/dhcpv4/bsdp/bsdp_option_reply_port_test.go new file mode 100644 index 0000000..0c9e03a --- /dev/null +++ b/dhcpv4/bsdp/bsdp_option_reply_port_test.go @@ -0,0 +1,44 @@ +// +build darwin + +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, 2, o.Length(), "Length") + require.Equal(t, []byte{byte(OptionReplyPort), 2, 4, 210}, o.ToBytes(), "ToBytes") +} + +func TestParseOptReplyPort(t *testing.T) { + data := []byte{byte(OptionReplyPort), 2, 0, 1} + o, err := ParseOptReplyPort(data) + require.NoError(t, err) + require.Equal(t, &OptReplyPort{1}, o) + + // Short byte stream + data = []byte{byte(OptionReplyPort), 2} + _, err = ParseOptReplyPort(data) + require.Error(t, err, "should get error from short byte stream") + + // Wrong code + data = []byte{54, 2, 1, 0} + _, err = ParseOptReplyPort(data) + require.Error(t, err, "should get error from wrong code") + + // Bad length + data = []byte{byte(OptionReplyPort), 4, 1, 0} + _, 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 new file mode 100644 index 0000000..6f426d2 --- /dev/null +++ b/dhcpv4/bsdp/bsdp_option_selected_boot_image_id.go @@ -0,0 +1,59 @@ +// +build darwin + +package bsdp + +import ( + "fmt" + + "github.com/insomniacslk/dhcp/dhcpv4" +) + +// Implements the BSDP option selected boot image ID, which tells the server +// which boot image has been selected by the client. + +// OptSelectedBootImageID contains the selected boot image ID. +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) { + if len(data) < 6 { + return nil, dhcpv4.ErrShortByteStream + } + code := dhcpv4.OptionCode(data[0]) + if code != OptionSelectedBootImageID { + return nil, fmt.Errorf("expected option %v, got %v instead", OptionSelectedBootImageID, code) + } + length := int(data[1]) + if length != 4 { + return nil, fmt.Errorf("expected length 4, got %d instead", length) + } + id, err := BootImageIDFromBytes(data[2:6]) + if err != nil { + return nil, err + } + return &OptSelectedBootImageID{*id}, nil +} + +// 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 { + serializedID := o.ID.ToBytes() + return append([]byte{byte(o.Code()), byte(len(serializedID))}, serializedID...) +} + +// 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()) +} + +// Length returns the length of the data portion of this option. +func (o *OptSelectedBootImageID) Length() int { + return 4 +} diff --git a/dhcpv4/bsdp/bsdp_option_selected_boot_image_id_test.go b/dhcpv4/bsdp/bsdp_option_selected_boot_image_id_test.go new file mode 100644 index 0000000..9087076 --- /dev/null +++ b/dhcpv4/bsdp/bsdp_option_selected_boot_image_id_test.go @@ -0,0 +1,56 @@ +// +build darwin + +package bsdp + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +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, 4, o.Length(), "Length") + expectedBytes := []byte{byte(OptionSelectedBootImageID), 4} + require.Equal(t, append(expectedBytes, b.ToBytes()...), o.ToBytes(), "ToBytes") +} + +func TestParseOptSelectedBootImageID(t *testing.T) { + b := BootImageID{IsInstall: true, ImageType: BootImageTypeMacOSX, Index: 1001} + bootImageBytes := b.ToBytes() + data := append([]byte{byte(OptionSelectedBootImageID), 4}, bootImageBytes...) + o, err := ParseOptSelectedBootImageID(data) + require.NoError(t, err) + require.Equal(t, &OptSelectedBootImageID{b}, o) + + // Short byte stream + data = []byte{byte(OptionSelectedBootImageID), 4} + _, err = ParseOptSelectedBootImageID(data) + require.Error(t, err, "should get error from short byte stream") + + // Wrong code + data = []byte{54, 2, 1, 0, 0, 0} + _, err = ParseOptSelectedBootImageID(data) + require.Error(t, err, "should get error from wrong code") + + // Bad length + data = []byte{byte(OptionSelectedBootImageID), 5, 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_version.go b/dhcpv4/bsdp/bsdp_option_version.go new file mode 100644 index 0000000..15acb86 --- /dev/null +++ b/dhcpv4/bsdp/bsdp_option_version.go @@ -0,0 +1,59 @@ +// +build darwin + +package bsdp + +import ( + "fmt" + + "github.com/insomniacslk/dhcp/dhcpv4" +) + +// Implements the BSDP option version. Can be one of 1.0 or 1.1 + +// Specific versions. +var ( + Version1_0 = []byte{1, 0} + Version1_1 = []byte{1, 1} +) + +// OptVersion represents a BSDP protocol version. +type OptVersion struct { + Version []byte +} + +// ParseOptVersion constructs an OptVersion struct from a sequence of +// bytes and returns it, or an error. +func ParseOptVersion(data []byte) (*OptVersion, error) { + if len(data) < 4 { + return nil, dhcpv4.ErrShortByteStream + } + code := dhcpv4.OptionCode(data[0]) + if code != OptionVersion { + return nil, fmt.Errorf("expected option %v, got %v instead", OptionVersion, code) + } + length := int(data[1]) + if length != 2 { + return nil, fmt.Errorf("expected length 2, got %d instead", length) + } + return &OptVersion{data[2:4]}, nil +} + +// Code returns the option code. +func (o *OptVersion) Code() dhcpv4.OptionCode { + return OptionVersion +} + +// ToBytes returns a serialized stream of bytes for this option. +func (o *OptVersion) ToBytes() []byte { + return append([]byte{byte(o.Code()), 2}, o.Version...) +} + +// String returns a human-readable string for this option. +func (o *OptVersion) String() string { + return fmt.Sprintf("BSDP Version -> %v.%v", o.Version[0], o.Version[1]) +} + +// Length returns the length of the data portion of this option. +func (o *OptVersion) Length() int { + return 2 +} diff --git a/dhcpv4/bsdp/bsdp_option_version_test.go b/dhcpv4/bsdp/bsdp_option_version_test.go new file mode 100644 index 0000000..8c09c6a --- /dev/null +++ b/dhcpv4/bsdp/bsdp_option_version_test.go @@ -0,0 +1,44 @@ +// +build darwin + +package bsdp + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOptVersionInterfaceMethods(t *testing.T) { + o := OptVersion{Version1_1} + require.Equal(t, OptionVersion, o.Code(), "Code") + require.Equal(t, 2, o.Length(), "Length") + require.Equal(t, []byte{2, 2, 1, 1}, o.ToBytes(), "ToBytes") +} + +func TestParseOptVersion(t *testing.T) { + data := []byte{2, 2, 1, 1} + o, err := ParseOptVersion(data) + require.NoError(t, err) + require.Equal(t, &OptVersion{Version1_1}, o) + + // Short byte stream + data = []byte{2, 2} + _, err = ParseOptVersion(data) + require.Error(t, err, "should get error from short byte stream") + + // Wrong code + data = []byte{54, 2, 1, 0} + _, err = ParseOptVersion(data) + require.Error(t, err, "should get error from wrong code") + + // Bad length + data = []byte{2, 4, 1, 0} + _, err = ParseOptVersion(data) + require.Error(t, err, "should get error from bad length") +} + +func TestOptVersionString(t *testing.T) { + // known + o := OptVersion{Version1_1} + require.Equal(t, "BSDP Version -> 1.1", o.String()) +} diff --git a/dhcpv4/bsdp/bsdp_test.go b/dhcpv4/bsdp/bsdp_test.go index e323b52..ae9060d 100644 --- a/dhcpv4/bsdp/bsdp_test.go +++ b/dhcpv4/bsdp/bsdp_test.go @@ -9,127 +9,6 @@ import ( "github.com/stretchr/testify/require" ) -/* - * BootImageID - */ -func TestBootImageIDToBytes(t *testing.T) { - b := BootImageID{ - IsInstall: true, - ImageType: BootImageTypeMacOSX, - Index: 0x1000, - } - actual := b.ToBytes() - expected := []byte{0x81, 0, 0x10, 0} - require.Equal(t, expected, actual) - - b.IsInstall = false - actual = b.ToBytes() - expected = []byte{0x01, 0, 0x10, 0} - require.Equal(t, expected, actual) -} - -func TestBootImageIDFromBytes(t *testing.T) { - b := BootImageID{ - IsInstall: false, - ImageType: BootImageTypeMacOSX, - Index: 0x1000, - } - newBootImage, err := BootImageIDFromBytes(b.ToBytes()) - require.NoError(t, err) - require.Equal(t, b, *newBootImage) - - b = BootImageID{ - IsInstall: true, - ImageType: BootImageTypeMacOSX, - Index: 0x1011, - } - newBootImage, err = BootImageIDFromBytes(b.ToBytes()) - require.NoError(t, err) - require.Equal(t, b, *newBootImage) -} - -func TestBootImageIDFromBytesFail(t *testing.T) { - serialized := []byte{0x81, 0, 0x10} // intentionally left short - deserialized, err := BootImageIDFromBytes(serialized) - require.Nil(t, deserialized) - require.Error(t, err) -} - -/* - * BootImage - */ -func TestBootImageToBytes(t *testing.T) { - b := BootImage{ - ID: BootImageID{ - IsInstall: true, - ImageType: BootImageTypeMacOSX, - Index: 0x1000, - }, - Name: "bsdp-1", - } - expected := []byte{ - 0x81, 0, 0x10, 0, // boot image ID - 6, // len(Name) - 98, 115, 100, 112, 45, 49, // byte-encoding of Name - } - actual := b.ToBytes() - require.Equal(t, expected, actual) - - b = BootImage{ - ID: BootImageID{ - IsInstall: false, - ImageType: BootImageTypeMacOSX, - Index: 0x1010, - }, - Name: "bsdp-21", - } - expected = []byte{ - 0x1, 0, 0x10, 0x10, // boot image ID - 7, // len(Name) - 98, 115, 100, 112, 45, 50, 49, // byte-encoding of Name - } - actual = b.ToBytes() - require.Equal(t, expected, actual) -} - -func TestBootImageFromBytes(t *testing.T) { - input := []byte{ - 0x1, 0, 0x10, 0x10, // boot image ID - 7, // len(Name) - 98, 115, 100, 112, 45, 50, 49, // byte-encoding of Name - } - b, err := BootImageFromBytes(input) - require.NoError(t, err) - expectedBootImage := BootImage{ - ID: BootImageID{ - IsInstall: false, - ImageType: BootImageTypeMacOSX, - Index: 0x1010, - }, - Name: "bsdp-21", - } - require.Equal(t, expectedBootImage, *b) -} - -func TestBootImageFromBytesOnlyBootImageID(t *testing.T) { - // Only a BootImageID, nothing else. - input := []byte{0x1, 0, 0x10, 0x10} - b, err := BootImageFromBytes(input) - require.Nil(t, b) - require.Error(t, err) -} - -func TestBootImageFromBytesShortBootImage(t *testing.T) { - input := []byte{ - 0x1, 0, 0x10, 0x10, // boot image ID - 7, // len(Name) - 98, 115, 100, 112, 45, 50, // Name bytes (intentionally off-by-one) - } - b, err := BootImageFromBytes(input) - require.Nil(t, b) - require.Error(t, err) -} - func TestParseBootImageSingleBootImage(t *testing.T) { input := []byte{ 0x1, 0, 0x10, 0x10, // boot image ID diff --git a/dhcpv4/bsdp/types.go b/dhcpv4/bsdp/types.go index cc721b8..d04d6ef 100644 --- a/dhcpv4/bsdp/types.go +++ b/dhcpv4/bsdp/types.go @@ -22,34 +22,6 @@ const ( OptionMachineName dhcpv4.OptionCode = 130 ) -// Versions -var ( - Version1_0 = []byte{1, 0} - Version1_1 = []byte{1, 1} -) - -// MessageType represents the different BSDP message types. -type MessageType byte - -// BSDP Message types - e.g. LIST, SELECT, FAILED -const ( - MessageTypeList MessageType = iota + 1 - MessageTypeSelect - MessageTypeFailed -) - -// BootImageType represents the different BSDP boot image types. -type BootImageType byte - -// Different types of BootImages - e.g. for different flavors of macOS. -const ( - BootImageTypeMacOS9 BootImageType = iota - BootImageTypeMacOSX - BootImageTypeMacOSXServer - BootImageTypeHardwareDiagnostics - // 0x4 - 0x7f are reserved for future use. -) - // OptionCodeToString maps BSDP OptionCodes to human-readable strings // describing what they are. var OptionCodeToString = map[dhcpv4.OptionCode]string{ |