summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--dhcpv4/bsdp/boot_image.go113
-rw-r--r--dhcpv4/bsdp/boot_image_test.go130
-rw-r--r--dhcpv4/bsdp/bsdp.go116
-rw-r--r--dhcpv4/bsdp/bsdp_option_message_type.go75
-rw-r--r--dhcpv4/bsdp/bsdp_option_message_type_test.go48
-rw-r--r--dhcpv4/bsdp/bsdp_option_reply_port.go60
-rw-r--r--dhcpv4/bsdp/bsdp_option_reply_port_test.go44
-rw-r--r--dhcpv4/bsdp/bsdp_option_selected_boot_image_id.go59
-rw-r--r--dhcpv4/bsdp/bsdp_option_selected_boot_image_id_test.go56
-rw-r--r--dhcpv4/bsdp/bsdp_option_version.go59
-rw-r--r--dhcpv4/bsdp/bsdp_option_version_test.go44
-rw-r--r--dhcpv4/bsdp/bsdp_test.go121
-rw-r--r--dhcpv4/bsdp/types.go28
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{