summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--dhcpv4/bsdp/bsdp.go90
-rw-r--r--dhcpv4/bsdp/bsdp_test.go177
-rw-r--r--dhcpv4/bsdp/types.go2
-rw-r--r--dhcpv4/bsdp/vendor_class_identifier_darwin.go2
4 files changed, 257 insertions, 14 deletions
diff --git a/dhcpv4/bsdp/bsdp.go b/dhcpv4/bsdp/bsdp.go
index bf84508..e15c59c 100644
--- a/dhcpv4/bsdp/bsdp.go
+++ b/dhcpv4/bsdp/bsdp.go
@@ -14,6 +14,20 @@ import (
// prefer this BSDP-specific option over the DHCP standard option.
const MaxDHCPMessageSize = 1500
+// AppleVendorID is the string constant set in the vendor class identifier (DHCP
+// option 60) that is sent by the server.
+const AppleVendorID = "AAPLBSDPC"
+
+// ReplyConfig is a struct containing some common configuration values for a
+// BSDP reply (ACK).
+type ReplyConfig struct {
+ ServerIP net.IP
+ ServerHostname, BootFileName string
+ ServerPriority int
+ Images []BootImage
+ DefaultImage, SelectedImage *BootImage
+}
+
// 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) {
@@ -161,3 +175,79 @@ func InformSelectForAck(ack dhcpv4.DHCPv4, replyPort uint16, selectedImage BootI
d.AddOption(&OptVendorSpecificInformation{vendorOpts})
return 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) {
+ 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)
+ if err != nil {
+ return nil, err
+ }
+ reply.SetClientIPAddr(inform.ClientIPAddr())
+ reply.SetYourIPAddr(net.IPv4zero)
+ reply.SetGatewayIPAddr(inform.GatewayIPAddr())
+ reply.SetServerIPAddr(config.ServerIP)
+ reply.SetServerHostName([]byte(config.ServerHostname))
+
+ reply.AddOption(&dhcpv4.OptMessageType{MessageType: dhcpv4.MessageTypeAck})
+ reply.AddOption(&dhcpv4.OptServerIdentifier{ServerID: config.ServerIP})
+ reply.AddOption(&dhcpv4.OptClassIdentifier{Identifier: AppleVendorID})
+
+ // BSDP opts.
+ vendorOpts := []dhcpv4.Option{
+ &OptMessageType{Type: MessageTypeList},
+ &OptServerPriority{Priority: config.ServerPriority},
+ &OptDefaultBootImageID{ID: config.DefaultImage.ID},
+ &OptBootImageList{Images: config.Images},
+ }
+ if config.SelectedImage != nil {
+ vendorOpts = append(vendorOpts, &OptSelectedBootImageID{ID: config.SelectedImage.ID})
+ }
+ reply.AddOption(&OptVendorSpecificInformation{Options: vendorOpts})
+
+ reply.AddOption(&dhcpv4.OptionGeneric{OptionCode: dhcpv4.OptionEnd})
+ return 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) {
+ 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)
+ if err != nil {
+ return nil, err
+ }
+
+ reply.SetClientIPAddr(inform.ClientIPAddr())
+ reply.SetYourIPAddr(net.IPv4zero)
+ reply.SetGatewayIPAddr(inform.GatewayIPAddr())
+ reply.SetServerIPAddr(config.ServerIP)
+ reply.SetServerHostName([]byte(config.ServerHostname))
+ reply.SetBootFileName([]byte(config.BootFileName))
+
+ reply.AddOption(&dhcpv4.OptMessageType{MessageType: dhcpv4.MessageTypeAck})
+ reply.AddOption(&dhcpv4.OptServerIdentifier{ServerID: config.ServerIP})
+ reply.AddOption(&dhcpv4.OptClassIdentifier{Identifier: AppleVendorID})
+
+ // BSDP opts.
+ reply.AddOption(&OptVendorSpecificInformation{
+ Options: []dhcpv4.Option{
+ &OptMessageType{Type: MessageTypeSelect},
+ &OptSelectedBootImageID{ID: config.SelectedImage.ID},
+ },
+ })
+
+ reply.AddOption(&dhcpv4.OptionGeneric{OptionCode: dhcpv4.OptionEnd})
+ return reply, nil
+}
diff --git a/dhcpv4/bsdp/bsdp_test.go b/dhcpv4/bsdp/bsdp_test.go
index 4cc55a6..7da4dfa 100644
--- a/dhcpv4/bsdp/bsdp_test.go
+++ b/dhcpv4/bsdp/bsdp_test.go
@@ -9,6 +9,14 @@ import (
"github.com/stretchr/testify/require"
)
+func RequireHasOption(t *testing.T, opts dhcpv4.OptionGetter, opt dhcpv4.Option) {
+ require.NotNil(t, opts, "must pass list of options")
+ require.NotNil(t, opt, "must pass option")
+ require.True(t, dhcpv4.HasOption(opts, opt.Code()))
+ actual := opts.GetOneOption(opt.Code())
+ require.Equal(t, opt, actual)
+}
+
func TestParseBootImageListFromAck(t *testing.T) {
expectedBootImages := []BootImage{
BootImage{
@@ -142,16 +150,10 @@ func TestInformSelectForAck_Broadcast(t *testing.T) {
require.True(t, dhcpv4.HasOption(m, dhcpv4.OptionVendorSpecificInformation))
opt = m.GetOneOption(dhcpv4.OptionVendorSpecificInformation)
vendorInfo := opt.(*OptVendorSpecificInformation)
- require.True(t, dhcpv4.HasOption(vendorInfo, OptionMessageType))
- opt = vendorInfo.GetOneOption(OptionMessageType)
- require.Equal(t, MessageTypeSelect, opt.(*OptMessageType).Type)
+ RequireHasOption(t, vendorInfo, &OptMessageType{Type: MessageTypeSelect})
require.True(t, dhcpv4.HasOption(vendorInfo, OptionVersion))
- require.True(t, dhcpv4.HasOption(vendorInfo, OptionSelectedBootImageID))
- opt = vendorInfo.GetOneOption(OptionSelectedBootImageID)
- require.Equal(t, bootImage.ID, opt.(*OptSelectedBootImageID).ID)
- require.True(t, dhcpv4.HasOption(vendorInfo, OptionServerIdentifier))
- opt = vendorInfo.GetOneOption(OptionServerIdentifier)
- require.True(t, serverID.Equal(opt.(*OptServerIdentifier).ServerID))
+ RequireHasOption(t, vendorInfo, &OptSelectedBootImageID{ID: bootImage.ID})
+ RequireHasOption(t, vendorInfo, &OptServerIdentifier{ServerID: serverID})
}
func TestInformSelectForAck_NoServerID(t *testing.T) {
@@ -214,7 +216,158 @@ func TestInformSelectForAck_ReplyPort(t *testing.T) {
require.True(t, dhcpv4.HasOption(m, dhcpv4.OptionVendorSpecificInformation))
opt := m.GetOneOption(dhcpv4.OptionVendorSpecificInformation)
vendorInfo := opt.(*OptVendorSpecificInformation)
- require.True(t, dhcpv4.HasOption(vendorInfo, OptionReplyPort))
- opt = vendorInfo.GetOneOption(OptionReplyPort)
- require.Equal(t, replyPort, opt.(*OptReplyPort).Port)
+ RequireHasOption(t, vendorInfo, &OptReplyPort{Port: replyPort})
+}
+
+func TestNewReplyForInformList_NoDefaultImage(t *testing.T) {
+ inform, _ := NewInformList(net.HardwareAddr{1, 2, 3, 4, 5, 6}, net.IP{1, 2, 3, 4}, dhcpv4.ClientPort)
+ _, err := NewReplyForInformList(inform, ReplyConfig{})
+ require.Error(t, err)
+}
+
+func TestNewReplyForInformList_NoImages(t *testing.T) {
+ inform, _ := NewInformList(net.HardwareAddr{1, 2, 3, 4, 5, 6}, net.IP{1, 2, 3, 4}, dhcpv4.ClientPort)
+ fakeImage := BootImage{
+ ID: BootImageID{ImageType: BootImageTypeMacOSX},
+ }
+ _, err := NewReplyForInformList(inform, ReplyConfig{
+ Images: []BootImage{},
+ DefaultImage: &fakeImage,
+ })
+ require.Error(t, err)
+
+ _, err = NewReplyForInformList(inform, ReplyConfig{
+ Images: nil,
+ SelectedImage: &fakeImage,
+ })
+ require.Error(t, err)
+}
+
+func TestNewReplyForInformList(t *testing.T) {
+ inform, _ := NewInformList(net.HardwareAddr{1, 2, 3, 4, 5, 6}, net.IP{1, 2, 3, 4}, dhcpv4.ClientPort)
+ images := []BootImage{
+ BootImage{
+ ID: BootImageID{
+ IsInstall: true,
+ ImageType: BootImageTypeMacOSX,
+ Index: 0x7070,
+ },
+ Name: "image-1",
+ },
+ BootImage{
+ ID: BootImageID{
+ IsInstall: true,
+ ImageType: BootImageTypeMacOSX,
+ Index: 0x8080,
+ },
+ Name: "image-2",
+ },
+ }
+ config := ReplyConfig{
+ Images: images,
+ DefaultImage: &images[0],
+ ServerIP: net.IP{9, 9, 9, 9},
+ ServerHostname: "bsdp.foo.com",
+ ServerPriority: 0x7070,
+ }
+ ack, err := NewReplyForInformList(inform, config)
+ require.NoError(t, err)
+ require.Equal(t, net.IP{1, 2, 3, 4}, ack.ClientIPAddr())
+ require.Equal(t, net.IPv4zero, ack.YourIPAddr())
+ require.Equal(t, "bsdp.foo.com", ack.ServerHostNameToString())
+
+ // Validate options.
+ RequireHasOption(t, ack, &dhcpv4.OptMessageType{MessageType: dhcpv4.MessageTypeAck})
+ RequireHasOption(t, ack, &dhcpv4.OptServerIdentifier{ServerID: net.IP{9, 9, 9, 9}})
+ RequireHasOption(t, ack, &dhcpv4.OptClassIdentifier{Identifier: AppleVendorID})
+ require.NotNil(t, ack.GetOneOption(dhcpv4.OptionVendorSpecificInformation))
+
+ // Ensure options terminated with End option.
+ require.Equal(t, &dhcpv4.OptionGeneric{OptionCode: dhcpv4.OptionEnd}, ack.Options()[len(ack.Options())-1])
+
+ // Vendor-specific options.
+ vendorOpts := ack.GetOneOption(dhcpv4.OptionVendorSpecificInformation).(*OptVendorSpecificInformation)
+ RequireHasOption(t, vendorOpts, &OptMessageType{Type: MessageTypeList})
+ RequireHasOption(t, vendorOpts, &OptDefaultBootImageID{ID: images[0].ID})
+ RequireHasOption(t, vendorOpts, &OptServerPriority{Priority: 0x7070})
+ RequireHasOption(t, vendorOpts, &OptBootImageList{Images: 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, &OptSelectedBootImageID{ID: images[0].ID})
+}
+
+func TestNewReplyForInformSelect_NoSelectedImage(t *testing.T) {
+ inform, _ := NewInformList(net.HardwareAddr{1, 2, 3, 4, 5, 6}, net.IP{1, 2, 3, 4}, dhcpv4.ClientPort)
+ _, err := NewReplyForInformSelect(inform, ReplyConfig{})
+ require.Error(t, err)
+}
+
+func TestNewReplyForInformSelect_NoImages(t *testing.T) {
+ inform, _ := NewInformList(net.HardwareAddr{1, 2, 3, 4, 5, 6}, net.IP{1, 2, 3, 4}, dhcpv4.ClientPort)
+ fakeImage := BootImage{
+ ID: BootImageID{ImageType: BootImageTypeMacOSX},
+ }
+ _, err := NewReplyForInformSelect(inform, ReplyConfig{
+ Images: []BootImage{},
+ SelectedImage: &fakeImage,
+ })
+ require.Error(t, err)
+
+ _, err = NewReplyForInformSelect(inform, ReplyConfig{
+ Images: nil,
+ SelectedImage: &fakeImage,
+ })
+ require.Error(t, err)
+}
+
+func TestNewReplyForInformSelect(t *testing.T) {
+ inform, _ := NewInformList(net.HardwareAddr{1, 2, 3, 4, 5, 6}, net.IP{1, 2, 3, 4}, dhcpv4.ClientPort)
+ images := []BootImage{
+ BootImage{
+ ID: BootImageID{
+ IsInstall: true,
+ ImageType: BootImageTypeMacOSX,
+ Index: 0x7070,
+ },
+ Name: "image-1",
+ },
+ BootImage{
+ ID: BootImageID{
+ IsInstall: true,
+ ImageType: BootImageTypeMacOSX,
+ Index: 0x8080,
+ },
+ Name: "image-2",
+ },
+ }
+ config := ReplyConfig{
+ Images: images,
+ SelectedImage: &images[0],
+ ServerIP: net.IP{9, 9, 9, 9},
+ ServerHostname: "bsdp.foo.com",
+ ServerPriority: 0x7070,
+ }
+ ack, err := NewReplyForInformSelect(inform, config)
+ require.NoError(t, err)
+ require.Equal(t, net.IP{1, 2, 3, 4}, ack.ClientIPAddr())
+ require.Equal(t, net.IPv4zero, ack.YourIPAddr())
+ require.Equal(t, "bsdp.foo.com", ack.ServerHostNameToString())
+
+ // Validate options.
+ RequireHasOption(t, ack, &dhcpv4.OptMessageType{MessageType: dhcpv4.MessageTypeAck})
+ RequireHasOption(t, ack, &dhcpv4.OptServerIdentifier{ServerID: net.IP{9, 9, 9, 9}})
+ RequireHasOption(t, ack, &dhcpv4.OptServerIdentifier{ServerID: net.IP{9, 9, 9, 9}})
+ RequireHasOption(t, ack, &dhcpv4.OptClassIdentifier{Identifier: AppleVendorID})
+ require.NotNil(t, ack.GetOneOption(dhcpv4.OptionVendorSpecificInformation))
+
+ // Ensure options are terminated with End option.
+ require.Equal(t, &dhcpv4.OptionGeneric{OptionCode: dhcpv4.OptionEnd}, ack.Options()[len(ack.Options())-1])
+
+ vendorOpts := ack.GetOneOption(dhcpv4.OptionVendorSpecificInformation).(*OptVendorSpecificInformation)
+ RequireHasOption(t, vendorOpts, &OptMessageType{Type: MessageTypeSelect})
+ RequireHasOption(t, vendorOpts, &OptSelectedBootImageID{ID: images[0].ID})
}
diff --git a/dhcpv4/bsdp/types.go b/dhcpv4/bsdp/types.go
index 4f1c140..ac4b05b 100644
--- a/dhcpv4/bsdp/types.go
+++ b/dhcpv4/bsdp/types.go
@@ -6,7 +6,7 @@ import "github.com/insomniacslk/dhcp/dhcpv4"
// 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
// system.
-const DefaultMacOSVendorClassIdentifier = "AAPLBSDP/i386/MacMini6,1"
+const DefaultMacOSVendorClassIdentifier = AppleVendorID + "/i386/MacMini6,1"
// Options (occur as sub-options of DHCP option 43).
const (
diff --git a/dhcpv4/bsdp/vendor_class_identifier_darwin.go b/dhcpv4/bsdp/vendor_class_identifier_darwin.go
index 530d843..d1b4c2b 100644
--- a/dhcpv4/bsdp/vendor_class_identifier_darwin.go
+++ b/dhcpv4/bsdp/vendor_class_identifier_darwin.go
@@ -13,5 +13,5 @@ func MakeVendorClassIdentifier() (string, error) {
if err != nil {
return "", err
}
- return fmt.Sprintf("AAPLBSDPC/i386/%s", hwModel), nil
+ return fmt.Sprintf("%s/i386/%s", AppleVendorID, hwModel), nil
}