diff options
Diffstat (limited to 'dhcpv4/bsdp')
-rw-r--r-- | dhcpv4/bsdp/bsdp.go | 90 | ||||
-rw-r--r-- | dhcpv4/bsdp/bsdp_test.go | 204 | ||||
-rw-r--r-- | dhcpv4/bsdp/types.go | 2 | ||||
-rw-r--r-- | dhcpv4/bsdp/vendor_class_identifier_darwin.go | 2 |
4 files changed, 296 insertions, 2 deletions
diff --git a/dhcpv4/bsdp/bsdp.go b/dhcpv4/bsdp/bsdp.go index 27de68e..2909ccd 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) { @@ -139,3 +153,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 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 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 ad2a265..f457631 100644 --- a/dhcpv4/bsdp/bsdp_test.go +++ b/dhcpv4/bsdp/bsdp_test.go @@ -1,9 +1,11 @@ package bsdp import ( + "net" "testing" "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/require" ) @@ -49,3 +51,205 @@ func TestNeedsReplyPort(t *testing.T) { require.False(t, needsReplyPort(0)) require.False(t, needsReplyPort(dhcpv4.ClientPort)) } + +// TODO(get9): Remove when #99 lands. +func newInform() *dhcpv4.DHCPv4 { + p, _ := dhcpv4.New() + p.SetClientIPAddr(net.IP{1, 2, 3, 4}) + p.SetGatewayIPAddr(net.IP{4, 3, 2, 1}) + p.SetHwType(iana.HwTypeEthernet) + hwAddr := [16]byte{1, 2, 3, 4, 5, 6} + p.SetClientHwAddr(hwAddr[:]) + p.SetHwAddrLen(6) + return p +} + +func TestNewReplyForInformList_NoDefaultImage(t *testing.T) { + inform := newInform() + _, err := NewReplyForInformList(inform, ReplyConfig{}) + require.Error(t, err) +} + +func TestNewReplyForInformList_NoImages(t *testing.T) { + inform := newInform() + fakeImage := BootImage{ + ID: BootImageID{ImageType: BootImageTypeMacOSX}, + } + _, err := NewReplyForInformList(inform, ReplyConfig{ + Images: []BootImage{}, + DefaultImage: &fakeImage, + }) + require.Error(t, err) +} + +// TODO (get9): clean up when #99 lands. +func TestNewReplyForInformList(t *testing.T) { + inform := newInform() + 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, net.IP{4, 3, 2, 1}, ack.GatewayIPAddr()) + require.Equal(t, "bsdp.foo.com", ack.ServerHostNameToString()) + + // Validate options. + require.Equal( + t, + &dhcpv4.OptMessageType{MessageType: dhcpv4.MessageTypeAck}, + ack.GetOneOption(dhcpv4.OptionDHCPMessageType).(*dhcpv4.OptMessageType), + ) + require.Equal( + t, + &dhcpv4.OptServerIdentifier{ServerID: net.IP{9, 9, 9, 9}}, + ack.GetOneOption(dhcpv4.OptionServerIdentifier).(*dhcpv4.OptServerIdentifier), + ) + require.Equal( + t, + &dhcpv4.OptClassIdentifier{Identifier: AppleVendorID}, + ack.GetOneOption(dhcpv4.OptionClassIdentifier).(*dhcpv4.OptClassIdentifier), + ) + require.NotNil(t, ack.GetOneOption(dhcpv4.OptionVendorSpecificInformation)) + require.Equal(t, &dhcpv4.OptionGeneric{OptionCode: dhcpv4.OptionEnd}, ack.Options()[len(ack.Options())-1]) + + vendorOpts := ack.GetOneOption(dhcpv4.OptionVendorSpecificInformation).(*OptVendorSpecificInformation) + require.Equal( + t, + &OptMessageType{Type: MessageTypeList}, + vendorOpts.GetOneOption(OptionMessageType).(*OptMessageType), + ) + require.Equal( + t, + &OptServerPriority{Priority: 0x7070}, + vendorOpts.GetOneOption(OptionServerPriority).(*OptServerPriority), + ) + require.Equal( + t, + &OptDefaultBootImageID{ID: images[0].ID}, + vendorOpts.GetOneOption(OptionDefaultBootImageID).(*OptDefaultBootImageID), + ) + require.Equal( + t, + &OptBootImageList{Images: images}, + vendorOpts.GetOneOption(OptionBootImageList).(*OptBootImageList), + ) + + // 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) + require.Equal( + t, + &OptSelectedBootImageID{ID: images[0].ID}, + vendorOpts.GetOneOption(OptionSelectedBootImageID).(*OptSelectedBootImageID), + ) +} + +func TestNewReplyForInformSelect_NoSelectedImage(t *testing.T) { + inform := newInform() + _, err := NewReplyForInformSelect(inform, ReplyConfig{}) + require.Error(t, err) +} + +func TestNewReplyForInformSelect_NoImages(t *testing.T) { + inform := newInform() + fakeImage := BootImage{ + ID: BootImageID{ImageType: BootImageTypeMacOSX}, + } + _, err := NewReplyForInformSelect(inform, ReplyConfig{ + Images: []BootImage{}, + SelectedImage: &fakeImage, + }) + require.Error(t, err) +} + +func TestNewReplyForInformSelect(t *testing.T) { + inform := newInform() + 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, net.IP{4, 3, 2, 1}, ack.GatewayIPAddr()) + require.Equal(t, "bsdp.foo.com", ack.ServerHostNameToString()) + + // Validate options. + require.Equal( + t, + &dhcpv4.OptMessageType{MessageType: dhcpv4.MessageTypeAck}, + ack.GetOneOption(dhcpv4.OptionDHCPMessageType).(*dhcpv4.OptMessageType), + ) + require.Equal( + t, + &dhcpv4.OptServerIdentifier{ServerID: net.IP{9, 9, 9, 9}}, + ack.GetOneOption(dhcpv4.OptionServerIdentifier).(*dhcpv4.OptServerIdentifier), + ) + require.Equal( + t, + &dhcpv4.OptClassIdentifier{Identifier: AppleVendorID}, + ack.GetOneOption(dhcpv4.OptionClassIdentifier).(*dhcpv4.OptClassIdentifier), + ) + require.NotNil(t, ack.GetOneOption(dhcpv4.OptionVendorSpecificInformation)) + require.Equal(t, &dhcpv4.OptionGeneric{OptionCode: dhcpv4.OptionEnd}, ack.Options()[len(ack.Options())-1]) + + vendorOpts := ack.GetOneOption(dhcpv4.OptionVendorSpecificInformation).(*OptVendorSpecificInformation) + require.Equal( + t, + &OptMessageType{Type: MessageTypeSelect}, + vendorOpts.GetOneOption(OptionMessageType).(*OptMessageType), + ) + require.Equal( + t, + &OptSelectedBootImageID{ID: images[0].ID}, + vendorOpts.GetOneOption(OptionSelectedBootImageID).(*OptSelectedBootImageID), + ) +} 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 } |