summaryrefslogtreecommitdiffhomepage
path: root/dhcpv4/bsdp
diff options
context:
space:
mode:
Diffstat (limited to 'dhcpv4/bsdp')
-rw-r--r--dhcpv4/bsdp/bsdp.go90
-rw-r--r--dhcpv4/bsdp/bsdp_test.go204
-rw-r--r--dhcpv4/bsdp/types.go2
-rw-r--r--dhcpv4/bsdp/vendor_class_identifier_darwin.go2
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
}