diff options
-rw-r--r-- | dhcpv4/bsdp/bsdp.go | 90 | ||||
-rw-r--r-- | dhcpv4/bsdp/bsdp_test.go | 177 | ||||
-rw-r--r-- | dhcpv4/bsdp/types.go | 2 | ||||
-rw-r--r-- | dhcpv4/bsdp/vendor_class_identifier_darwin.go | 2 |
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 } |