diff options
-rw-r--r-- | dhcpv4/bsdp.go | 349 | ||||
-rw-r--r-- | dhcpv4/bsdp/bsdp.go | 336 | ||||
-rw-r--r-- | dhcpv4/bsdp/bsdp_test.go | 362 | ||||
-rw-r--r-- | dhcpv4/bsdp/client.go (renamed from dhcpv4/bsdp_client.go) | 52 | ||||
-rw-r--r-- | dhcpv4/bsdp/types.go | 68 | ||||
-rw-r--r-- | dhcpv4/bsdp_test.go | 297 | ||||
-rw-r--r-- | dhcpv4/client.go | 12 | ||||
-rw-r--r-- | dhcpv4/dhcpv4.go | 44 | ||||
-rw-r--r-- | dhcpv4/dhcpv4_test.go | 23 | ||||
-rw-r--r-- | dhcpv4/options.go | 43 | ||||
-rw-r--r-- | dhcpv4/options_test.go | 42 | ||||
-rw-r--r-- | dhcpv4/types.go | 24 |
12 files changed, 852 insertions, 800 deletions
diff --git a/dhcpv4/bsdp.go b/dhcpv4/bsdp.go deleted file mode 100644 index 4096ce6..0000000 --- a/dhcpv4/bsdp.go +++ /dev/null @@ -1,349 +0,0 @@ -// +build darwin - -package dhcpv4 - -// Implements Apple's netboot protocol BSDP (Boot Service Discovery Protocol). -// Canonical implementation is defined here: -// http://opensource.apple.com/source/bootp/bootp-198.1/Documentation/BSDP.doc - -import ( - "encoding/binary" - "fmt" - "syscall" -) - -// Options (occur as sub-options of DHCP option 43). -const ( - BSDPOptionMessageType OptionCode = iota + 1 - BSDPOptionVersion - BSDPOptionServerIdentifier - BSDPOptionServerPriority - BSDPOptionReplyPort - BSDPOptionBootImageListPath // Not used - BSDPOptionDefaultBootImageID - BSDPOptionSelectedBootImageID - BSDPOptionBootImageList - BSDPOptionNetboot1_0Firmware - BSDPOptionBootImageAttributesFilterList - BSDPOptionShadowMountPath OptionCode = 128 - BSDPOptionShadowFilePath OptionCode = 129 - BSDPOptionMachineName OptionCode = 130 -) - -// Versions -var ( - BSDPVersion1_0 = []byte{1, 0} - BSDPVersion1_1 = []byte{1, 1} -) - -// BSDP message types -const ( - BSDPMessageTypeList byte = iota + 1 - BSDPMessageTypeSelect - BSDPMessageTypeFailed -) - -// Boot image kinds -const ( - BSDPBootImageMacOS9 byte = iota - BSDPBootImageMacOSX - BSDPBootImageMacOSXServer - BSDPBootImageHardwareDiagnostics - // 0x4 - 0x7f are reserved for future use. -) - -// 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 - imageKind byte - index uint16 -} - -// toBytes serializes a BootImageID to network-order bytes. -func (b BootImageID) toBytes() (bytes []byte) { - bytes = make([]byte, 4) - // Attributes. - if b.isInstall { - bytes[0] |= 0x80 - } - bytes[0] |= b.imageKind - - // Index - binary.BigEndian.PutUint16(bytes[2:], b.index) - return -} - -// BootImageIDFromBytes deserializes a collection of 4 bytes to a BootImageID. -func bootImageIDFromBytes(bytes []byte) BootImageID { - return BootImageID{ - isInstall: bytes[0]&0x80 != 0, - imageKind: bytes[0] & 0x7f, - index: binary.BigEndian.Uint16(bytes[2:]), - } -} - -// BootImage describes a boot image - contains the boot image ID and the name. -type BootImage struct { - ID BootImageID - // This is a utf-8 string. - Name string -} - -// toBytes converts a BootImage to a slice of bytes. -func (b *BootImage) toBytes() (bytes []byte) { - idBytes := b.ID.toBytes() - bytes = append(bytes, idBytes[:]...) - bytes = append(bytes, byte(len(b.Name))) - bytes = append(bytes, []byte(b.Name)...) - return -} - -// BootImageFromBytes returns a deserialized BootImage struct from bytes as well -// as the number of bytes read from the slice. -func bootImageFromBytes(bytes []byte) (*BootImage, int, error) { - // If less than length of boot image ID and count, it's probably invalid. - if len(bytes) < 5 { - return nil, 0, fmt.Errorf("not enough bytes for BootImage") - } - imageID := bootImageIDFromBytes(bytes[:4]) - nameLength := int(bytes[4]) - if 5+nameLength > len(bytes) { - return nil, 0, fmt.Errorf("not enough bytes for BootImage") - } - name := string(bytes[5 : 5+nameLength]) - return &BootImage{ID: imageID, Name: name}, 5 + nameLength, nil -} - -// makeVendorClassIdentifier calls the sysctl syscall on macOS to get the -// platform model. -func makeVendorClassIdentifier() (string, error) { - // Fetch hardware model for class ID. - hwModel, err := syscall.Sysctl("hw.model") - if err != nil { - return "", err - } - vendorClassID := fmt.Sprintf("AAPLBSDPC/i386/%s", hwModel) - return vendorClassID, nil -} - -// parseBootImagesFromBSDPOption parses data from the BSDPOptionBootImageList -// option and returns a list of BootImages. -func parseBootImagesFromBSDPOption(data []byte) ([]BootImage, error) { - // Should at least have the # bytes of boot images. - if len(data) < 4 { - return nil, fmt.Errorf("invalid length boot image list") - } - - readByteCount := 0 - start := data - var bootImages []BootImage - for { - bootImage, readBytes, err := bootImageFromBytes(start) - if err != nil { - return nil, err - } - bootImages = append(bootImages, *bootImage) - readByteCount += readBytes - if readByteCount+1 >= len(data) { - break - } - start = start[readByteCount:] - } - - return bootImages, nil -} - -// parseVendorOptionsFromOptions extracts the sub-options list of the vendor- -// specific options from the larger DHCP options list. -func parseVendorOptionsFromOptions(options []Option) []Option { - var vendorOpts []Option - var err error - for _, opt := range options { - if opt.Code == OptionVendorSpecificInformation { - vendorOpts, err = OptionsFromBytes(opt.Data) - if err != nil { - return []Option{} - } - break - } - } - return vendorOpts -} - -// 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) ([]BootImage, error) { - var bootImages []BootImage - vendorOpts := parseVendorOptionsFromOptions(ack.options) - for _, opt := range vendorOpts { - if opt.Code == BSDPOptionBootImageList { - images, err := parseBootImagesFromBSDPOption(opt.Data) - if err != nil { - return nil, err - } - bootImages = append(bootImages, images...) - } - } - - return bootImages, nil -} - -// NewInformListForInterface creates a new INFORM packet for interface ifname -// with configuration options specified by config. -func NewInformListForInterface(iface string, replyPort uint16) (*DHCPv4, error) { - d, err := NewInformForInterface(iface /* needsBroadcast */, false) - if err != nil { - return nil, err - } - - // These are vendor-specific options used to pass along BSDP information. - vendorOpts := []Option{ - Option{ - Code: BSDPOptionMessageType, - Data: []byte{BSDPMessageTypeList}, - }, - Option{ - Code: BSDPOptionVersion, - Data: BSDPVersion1_1, - }, - } - - // If specified, replyPort MUST be a priviledged port. - if replyPort != 0 && replyPort != ClientPort { - if replyPort >= 1024 { - return nil, fmt.Errorf("replyPort must be a priviledged port (< 1024)") - } - bytes := make([]byte, 3) - bytes[0] = 2 - binary.BigEndian.PutUint16(bytes[1:], replyPort) - d.AddOption(Option{ - Code: BSDPOptionReplyPort, - Data: bytes, - }) - } - d.AddOption(Option{ - Code: OptionVendorSpecificInformation, - Data: OptionsToBytes(vendorOpts), - }) - - d.AddOption(Option{ - Code: OptionParameterRequestList, - Data: []byte{OptionVendorSpecificInformation, OptionClassIdentifier}, - }) - - u16 := make([]byte, 2) - binary.BigEndian.PutUint16(u16, 1500) - d.AddOption(Option{ - Code: OptionMaximumDHCPMessageSize, - Data: u16, - }) - - vendorClassID, err := makeVendorClassIdentifier() - if err != nil { - return nil, err - } - d.AddOption(Option{ - Code: OptionClassIdentifier, - Data: []byte(vendorClassID), - }) - - d.AddOption(Option{Code: OptionEnd}) - return d, nil -} - -// InformSelectForAck constructs an INFORM[SELECT] packet given an ACK to the -// previously-sent INFORM[LIST] with BSDPConfig config. -func InformSelectForAck(ack DHCPv4, replyPort uint16, selectedImage BootImage) (*DHCPv4, error) { - d, err := New() - if err != nil { - return nil, err - } - d.SetOpcode(OpcodeBootRequest) - d.SetHwType(ack.HwType()) - d.SetHwAddrLen(ack.HwAddrLen()) - clientHwAddr := ack.ClientHwAddr() - d.SetClientHwAddr(clientHwAddr[:]) - d.SetTransactionID(ack.TransactionID()) - if ack.IsBroadcast() { - d.SetBroadcast() - } else { - d.SetUnicast() - } - - // Data for BSDPOptionSelectedBootImageID - vendorOpts := []Option{ - Option{ - Code: BSDPOptionMessageType, - Data: []byte{BSDPMessageTypeSelect}, - }, - Option{ - Code: BSDPOptionVersion, - Data: BSDPVersion1_1, - }, - Option{ - Code: BSDPOptionSelectedBootImageID, - Data: selectedImage.ID.toBytes(), - }, - } - - // Find server IP address - var serverIP []byte - for _, opt := range ack.options { - if opt.Code == OptionServerIdentifier { - serverIP = make([]byte, 4) - copy(serverIP, opt.Data) - } - } - if len(serverIP) == 0 { - return nil, fmt.Errorf("could not parse server identifier from ACK") - } - vendorOpts = append(vendorOpts, Option{ - Code: BSDPOptionServerIdentifier, - Data: serverIP, - }) - - // Validate replyPort if requested. - if replyPort != 0 && replyPort != ClientPort { - // replyPort MUST be a priviledged port. - if replyPort >= 1024 { - return nil, fmt.Errorf("replyPort must be a priviledged port") - } - bytes := make([]byte, 3) - bytes[0] = 2 - binary.BigEndian.PutUint16(bytes[1:], replyPort) - vendorOpts = append(vendorOpts, Option{ - Code: BSDPOptionReplyPort, - Data: bytes, - }) - } - - vendorClassID, err := makeVendorClassIdentifier() - if err != nil { - return nil, err - } - d.AddOption(Option{ - Code: OptionClassIdentifier, - Data: []byte(vendorClassID), - }) - d.AddOption(Option{ - Code: OptionParameterRequestList, - Data: []byte{ - OptionSubnetMask, - OptionRouter, - OptionBootfileName, - OptionVendorSpecificInformation, - OptionClassIdentifier, - }, - }) - d.AddOption(Option{ - Code: OptionDHCPMessageType, - Data: []byte{MessageTypeInform}, - }) - d.AddOption(Option{ - Code: OptionVendorSpecificInformation, - Data: OptionsToBytes(vendorOpts), - }) - d.AddOption(Option{Code: OptionEnd}) - return d, nil -} diff --git a/dhcpv4/bsdp/bsdp.go b/dhcpv4/bsdp/bsdp.go new file mode 100644 index 0000000..6c8d00f --- /dev/null +++ b/dhcpv4/bsdp/bsdp.go @@ -0,0 +1,336 @@ +// +build darwin + +package bsdp + +// Implements Apple's netboot protocol BSDP (Boot Service Discovery Protocol). +// Canonical implementation is defined here: +// http://opensource.apple.com/source/bootp/bootp-198.1/Documentation/BSDP.doc + +import ( + "encoding/binary" + "errors" + "fmt" + "log" + "net" + "syscall" + + "github.com/insomniacslk/dhcp/dhcpv4" +) + +// MaxDHCPMessageSize is the size set in DHCP option 57 (DHCP Maximum Message Size). +// BSDP includes its own sub-option (12) to indicate to NetBoot servers that the +// client can support larger message sizes, and modern NetBoot servers will +// 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) { + // Fetch hardware model for class ID. + hwModel, err := syscall.Sysctl("hw.model") + if err != nil { + return "", err + } + return fmt.Sprintf("AAPLBSDPC/i386/%s", hwModel), nil +} + +// ParseBootImagesFromOption parses data from the BSDPOptionBootImageList +// option and returns a list of BootImages. +func ParseBootImagesFromOption(data []byte) ([]BootImage, error) { + // Should at least have the # bytes of boot images. + if len(data) < 4 { + return nil, fmt.Errorf("invalid length boot image list") + } + + var ( + readByteCount = 0 + start = data + bootImages []BootImage + ) + for { + bootImage, err := BootImageFromBytes(start) + if err != nil { + return nil, err + } + bootImages = append(bootImages, *bootImage) + // Read BootImageID + name length + name + readByteCount += 4 + 1 + len(bootImage.Name) + if readByteCount+1 >= len(data) { + break + } + start = start[readByteCount:] + } + + return bootImages, nil +} + +// ParseVendorOptionsFromOptions extracts the sub-options list of the vendor- +// specific options from the larger DHCP options list. +// TODO: Implement options.GetOneOption for dhcpv4. +func ParseVendorOptionsFromOptions(options []dhcpv4.Option) []dhcpv4.Option { + var ( + vendorOpts []dhcpv4.Option + err error + ) + for _, opt := range options { + if opt.Code == dhcpv4.OptionVendorSpecificInformation { + vendorOpts, err = dhcpv4.OptionsFromBytesWithoutMagicCookie(opt.Data) + if err != nil { + log.Println("Warning: could not parse vendor options in DHCP options") + return []dhcpv4.Option{} + } + break + } + } + return vendorOpts +} + +// 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) { + var bootImages []BootImage + for _, opt := range ParseVendorOptionsFromOptions(ack.Options()) { + if opt.Code == OptionBootImageList { + images, err := ParseBootImagesFromOption(opt.Data) + if err != nil { + return nil, err + } + bootImages = append(bootImages, images...) + } + } + + return bootImages, nil +} + +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) { + d, err := dhcpv4.NewInformForInterface(iface /* needsBroadcast = */, false) + if err != nil { + return nil, err + } + + // Validate replyPort first + if needsReplyPort(replyPort) && replyPort >= 1024 { + return nil, errors.New("replyPort must be a privileged port") + } + + // These are vendor-specific options used to pass along BSDP information. + vendorOpts := []dhcpv4.Option{ + dhcpv4.Option{ + Code: OptionMessageType, + Data: []byte{byte(MessageTypeList)}, + }, + dhcpv4.Option{ + Code: OptionVersion, + Data: Version1_1, + }, + } + + if needsReplyPort(replyPort) { + vendorOpts = append(vendorOpts, + dhcpv4.Option{ + Code: OptionReplyPort, + Data: serializeReplyPort(replyPort), + }, + ) + } + d.AddOption(dhcpv4.Option{ + Code: dhcpv4.OptionVendorSpecificInformation, + Data: dhcpv4.OptionsToBytesWithoutMagicCookie(vendorOpts), + }) + + d.AddOption(dhcpv4.Option{ + Code: dhcpv4.OptionParameterRequestList, + Data: []byte{ + dhcpv4.OptionVendorSpecificInformation, + dhcpv4.OptionClassIdentifier, + }, + }) + + u16 := make([]byte, 2) + binary.BigEndian.PutUint16(u16, MaxDHCPMessageSize) + d.AddOption(dhcpv4.Option{ + Code: dhcpv4.OptionMaximumDHCPMessageSize, + Data: u16, + }) + + vendorClassID, err := makeVendorClassIdentifier() + if err != nil { + return nil, err + } + d.AddOption(dhcpv4.Option{ + Code: dhcpv4.OptionClassIdentifier, + Data: []byte(vendorClassID), + }) + + d.AddOption(dhcpv4.Option{Code: dhcpv4.OptionEnd}) + return d, nil +} + +// InformSelectForAck constructs an INFORM[SELECT] packet given an ACK to the +// previously-sent INFORM[LIST] with Config config. +func InformSelectForAck(ack dhcpv4.DHCPv4, replyPort uint16, selectedImage BootImage) (*dhcpv4.DHCPv4, error) { + d, err := dhcpv4.New() + if err != nil { + return nil, err + } + + if needsReplyPort(replyPort) && replyPort >= 1024 { + return nil, errors.New("replyPort must be a privilegded port") + } + d.SetOpcode(dhcpv4.OpcodeBootRequest) + d.SetHwType(ack.HwType()) + d.SetHwAddrLen(ack.HwAddrLen()) + clientHwAddr := ack.ClientHwAddr() + d.SetClientHwAddr(clientHwAddr[:]) + d.SetTransactionID(ack.TransactionID()) + if ack.IsBroadcast() { + d.SetBroadcast() + } else { + d.SetUnicast() + } + + // Data for OptionSelectedBootImageID + vendorOpts := []dhcpv4.Option{ + dhcpv4.Option{ + Code: OptionMessageType, + Data: []byte{byte(MessageTypeSelect)}, + }, + dhcpv4.Option{ + Code: OptionVersion, + Data: Version1_1, + }, + dhcpv4.Option{ + Code: OptionSelectedBootImageID, + Data: selectedImage.ID.ToBytes(), + }, + } + + // Find server IP address + var serverIP net.IP + // TODO replace this loop with `ack.GetOneOption(OptionBootImageList)` + for _, opt := range ack.Options() { + if opt.Code == dhcpv4.OptionServerIdentifier { + serverIP = net.IP(opt.Data) + } + } + if serverIP.To4() == nil { + return nil, fmt.Errorf("could not parse server identifier from ACK") + } + vendorOpts = append(vendorOpts, dhcpv4.Option{ + Code: OptionServerIdentifier, + Data: serverIP, + }) + + // Validate replyPort if requested. + if needsReplyPort(replyPort) { + vendorOpts = append(vendorOpts, dhcpv4.Option{ + Code: OptionReplyPort, + Data: serializeReplyPort(replyPort), + }) + } + + vendorClassID, err := makeVendorClassIdentifier() + if err != nil { + return nil, err + } + d.AddOption(dhcpv4.Option{ + Code: dhcpv4.OptionClassIdentifier, + Data: []byte(vendorClassID), + }) + d.AddOption(dhcpv4.Option{ + Code: dhcpv4.OptionParameterRequestList, + Data: []byte{ + dhcpv4.OptionSubnetMask, + dhcpv4.OptionRouter, + dhcpv4.OptionBootfileName, + dhcpv4.OptionVendorSpecificInformation, + dhcpv4.OptionClassIdentifier, + }, + }) + d.AddOption(dhcpv4.Option{ + Code: dhcpv4.OptionDHCPMessageType, + Data: []byte{byte(dhcpv4.MessageTypeInform)}, + }) + d.AddOption(dhcpv4.Option{ + Code: dhcpv4.OptionVendorSpecificInformation, + Data: dhcpv4.OptionsToBytesWithoutMagicCookie(vendorOpts), + }) + d.AddOption(dhcpv4.Option{Code: dhcpv4.OptionEnd}) + return d, nil +} diff --git a/dhcpv4/bsdp/bsdp_test.go b/dhcpv4/bsdp/bsdp_test.go new file mode 100644 index 0000000..b66efbc --- /dev/null +++ b/dhcpv4/bsdp/bsdp_test.go @@ -0,0 +1,362 @@ +package bsdp + +import ( + "testing" + + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/stretchr/testify/assert" +) + +/* + * BootImageID + */ +func TestBootImageIDToBytes(t *testing.T) { + b := BootImageID{ + IsInstall: true, + ImageType: BootImageTypeMacOSX, + Index: 0x1000, + } + actual := b.ToBytes() + expected := []byte{0x81, 0, 0x10, 0} + assert.Equal(t, actual, expected, "serialized BootImageID should be equal") + + b.IsInstall = false + actual = b.ToBytes() + expected = []byte{0x01, 0, 0x10, 0} + assert.Equal(t, actual, expected, "serialized BootImageID should be equal") +} + +func TestBootImageIDFromBytes(t *testing.T) { + b := BootImageID{ + IsInstall: false, + ImageType: BootImageTypeMacOSX, + Index: 0x1000, + } + newBootImage, err := BootImageIDFromBytes(b.ToBytes()) + assert.Nil(t, err, "error from BootImageIDFromBytes") + assert.Equal(t, b, *newBootImage, "deserialized BootImage should be equal") + + b = BootImageID{ + IsInstall: true, + ImageType: BootImageTypeMacOSX, + Index: 0x1011, + } + newBootImage, err = BootImageIDFromBytes(b.ToBytes()) + assert.Nil(t, err, "error from BootImageIDFromBytes") + assert.Equal(t, b, *newBootImage, "deserialized BootImage should be equal") +} + +func TestBootImageIDFromBytesFail(t *testing.T) { + serialized := []byte{0x81, 0, 0x10} // intentionally left short + deserialized, err := BootImageIDFromBytes(serialized) + assert.Nil(t, deserialized, "BootImageIDFromBytes should return nil on failed deserialization") + assert.NotNil(t, err, "BootImageIDFromBytes should return err on failed deserialization") +} + +/* + * 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() + assert.Equal(t, actual, expected, "serialized BootImage should be equal") + + 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() + assert.Equal(t, actual, expected, "serialized BootImage should be equal") +} + +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) + assert.Nil(t, err, "error while marshalling BootImage") + expectedBootImage := BootImage{ + ID: BootImageID{ + IsInstall: false, + ImageType: BootImageTypeMacOSX, + Index: 0x1010, + }, + Name: "bsdp-21", + } + assert.Equal(t, *b, expectedBootImage, "invalid marshalling of BootImage") +} + +func TestBootImageFromBytesOnlyBootImageID(t *testing.T) { + // Only a BootImageID, nothing else. + input := []byte{0x1, 0, 0x10, 0x10} + b, err := BootImageFromBytes(input) + assert.Nil(t, b, "short bytestream should return nil BootImageID") + assert.NotNil(t, err, "short bytestream should return error") +} + +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) + assert.Nil(t, b, "short bytestream should return nil BootImageID") + assert.NotNil(t, err, "short bytestream should return error") +} + +func TestParseBootImageSingleBootImage(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 + } + bs, err := ParseBootImagesFromOption(input) + assert.Nil(t, err, "parsing single boot image should not return error") + assert.Equal(t, len(bs), 1, "parsing single boot image should return 1") + b := bs[0] + expectedBootImage := BootImageID{ + IsInstall: false, + ImageType: BootImageTypeMacOSX, + Index: 0x1010, + } + assert.Equal(t, b.ID, expectedBootImage, "parsed BootImageIDs should be equal") + assert.Equal(t, b.Name, "bsdp-21", "BootImage name should be equal") +} + +func TestParseBootImageMultipleBootImage(t *testing.T) { + input := []byte{ + // boot image 1 + 0x1, 0, 0x10, 0x10, // boot image ID + 7, // len(Name) + 98, 115, 100, 112, 45, 50, 49, // byte-encoding of Name + + // boot image 2 + 0x82, 0, 0x11, 0x22, // boot image ID + 8, // len(Name) + 98, 115, 100, 112, 45, 50, 50, 50, // byte-encoding of Name + } + bs, err := ParseBootImagesFromOption(input) + assert.Nil(t, err, "parsing multiple BootImages should not return error") + assert.Equal(t, len(bs), 2, "parsing 2 BootImages should return 2") + b1 := bs[0] + b2 := bs[1] + expectedID1 := BootImageID{ + IsInstall: false, + ImageType: BootImageTypeMacOSX, + Index: 0x1010, + } + expectedID2 := BootImageID{ + IsInstall: true, + ImageType: BootImageTypeMacOSXServer, + Index: 0x1122, + } + assert.Equal(t, b1.ID, expectedID1, "first BootImageID should be equal") + assert.Equal(t, b2.ID, expectedID2, "second BootImageID should be equal") + assert.Equal(t, b1.Name, "bsdp-21", "first BootImage name should be equal") + assert.Equal(t, b2.Name, "bsdp-222", "second BootImage name should be equal") +} + +func TestParseBootImageFail(t *testing.T) { + _, err := ParseBootImagesFromOption([]byte{}) + assert.NotNil(t, err, "parseBootImages with empty arg") + + _, err = ParseBootImagesFromOption([]byte{1, 2, 3}) + assert.NotNil(t, err, "parseBootImages with short arg") + + _, err = ParseBootImagesFromOption([]byte{ + // boot image 1 + 0x1, 0, 0x10, 0x10, // boot image ID + 7, // len(Name) + 98, 115, 100, 112, 45, 50, // byte-encoding of Name (intentionally shorter) + + // boot image 2 + 0x82, 0, 0x11, 0x22, // boot image ID + 8, // len(Name) + 98, 115, 100, 112, 45, 50, 50, 50, // byte-encoding of Name + }) + assert.NotNil(t, err, "parseBootImages with short arg") +} + +/* + * ParseVendorOptionsFromOptions + */ +func TestParseVendorOptions(t *testing.T) { + vendorOpts := []dhcpv4.Option{ + dhcpv4.Option{ + Code: OptionMessageType, + Data: []byte{byte(MessageTypeList)}, + }, + dhcpv4.Option{ + Code: OptionVersion, + Data: Version1_0, + }, + } + recvOpts := []dhcpv4.Option{ + dhcpv4.Option{ + Code: dhcpv4.OptionDHCPMessageType, + Data: []byte{byte(dhcpv4.MessageTypeAck)}, + }, + dhcpv4.Option{ + Code: dhcpv4.OptionBroadcastAddress, + Data: []byte{0xff, 0xff, 0xff, 0xff}, + }, + dhcpv4.Option{ + Code: dhcpv4.OptionVendorSpecificInformation, + Data: dhcpv4.OptionsToBytesWithoutMagicCookie(vendorOpts), + }, + } + opts := ParseVendorOptionsFromOptions(recvOpts) + assert.Equal(t, opts, vendorOpts, "Parsed vendorOpts should be the same") +} + +func TestParseVendorOptionsFromOptionsNotPresent(t *testing.T) { + recvOpts := []dhcpv4.Option{ + dhcpv4.Option{ + Code: dhcpv4.OptionDHCPMessageType, + Data: []byte{byte(dhcpv4.MessageTypeAck)}, + }, + dhcpv4.Option{ + Code: dhcpv4.OptionBroadcastAddress, + Data: []byte{0xff, 0xff, 0xff, 0xff}, + }, + } + opts := ParseVendorOptionsFromOptions(recvOpts) + assert.Empty(t, opts, "vendor opts should be empty if not present in input") +} + +func TestParseVendorOptionsFromOptionsEmpty(t *testing.T) { + options := ParseVendorOptionsFromOptions([]dhcpv4.Option{}) + assert.Empty(t, options, "vendor opts should be empty if given an empty input") +} + +func TestParseVendorOptionsFromOptionsFail(t *testing.T) { + opts := []dhcpv4.Option{ + dhcpv4.Option{ + Code: dhcpv4.OptionVendorSpecificInformation, + Data: []byte{ + 0x1, 0x1, 0x1, // Option 1: LIST + 0x2, 0x2, 0x01, // Option 2: Version (intentionally left short) + }, + }, + } + vendorOpts := ParseVendorOptionsFromOptions(opts) + assert.Empty(t, vendorOpts, "vendor opts should be empty on parse error") +} + +/* + * ParseBootImageListFromAck + */ +func TestParseBootImageListFromAck(t *testing.T) { + bootImages := []BootImage{ + BootImage{ + ID: BootImageID{ + IsInstall: true, + ImageType: BootImageTypeMacOSX, + Index: 0x1010, + }, + Name: "bsdp-1", + }, + BootImage{ + ID: BootImageID{ + IsInstall: false, + ImageType: BootImageTypeMacOS9, + Index: 0x1111, + }, + Name: "bsdp-2", + }, + } + var bootImageBytes []byte + for _, image := range bootImages { + bootImageBytes = append(bootImageBytes, image.ToBytes()...) + } + ack, _ := dhcpv4.New() + ack.AddOption(dhcpv4.Option{ + Code: dhcpv4.OptionVendorSpecificInformation, + Data: dhcpv4.OptionsToBytesWithoutMagicCookie([]dhcpv4.Option{ + dhcpv4.Option{ + Code: OptionBootImageList, + Data: bootImageBytes, + }, + }), + }) + + images, err := ParseBootImageListFromAck(*ack) + assert.Nil(t, err, "error from ParseBootImageListFromAck") + assert.NotNil(t, images, "parsed boot images from ack") + assert.Equal(t, images, bootImages, "should get same BootImages") +} + +func TestParseBootImageListFromAckNoVendorOption(t *testing.T) { + ack, _ := dhcpv4.New() + ack.AddOption(dhcpv4.Option{ + Code: OptionMessageType, + Data: []byte{byte(dhcpv4.MessageTypeAck)}, + }) + images, err := ParseBootImageListFromAck(*ack) + assert.Nil(t, err, "no vendor extensions should not return error") + assert.Empty(t, images, "should not get images from ACK without Vendor extensions") +} + +func TestParseBootImageListFromAckFail(t *testing.T) { + ack, _ := dhcpv4.New() + ack.AddOption(dhcpv4.Option{ + Code: OptionMessageType, + Data: []byte{byte(dhcpv4.MessageTypeAck)}, + }) + ack.AddOption(dhcpv4.Option{ + Code: dhcpv4.OptionVendorSpecificInformation, + Data: dhcpv4.OptionsToBytesWithoutMagicCookie([]dhcpv4.Option{ + dhcpv4.Option{ + Code: OptionBootImageList, + Data: []byte{ + // boot image 1 + 0x1, 0, 0x10, 0x10, // boot image ID + 7, // len(Name) + 98, 115, 100, 112, 45, 49, // byte-encoding of Name (intentionally short) + + // boot image 2 + 0x82, 0, 0x11, 0x22, // boot image ID + 8, // len(Name) + 98, 115, 100, 112, 45, 50, 50, 50, // byte-encoding of Name + }, + }, + }), + }) + + images, err := ParseBootImageListFromAck(*ack) + assert.Nil(t, images, "should get nil on parse error") + assert.NotNil(t, err, "should get error on parse error") +} + +/* + * Private funcs + */ +func TestNeedsReplyPort(t *testing.T) { + assert.True(t, needsReplyPort(123), "") + assert.False(t, needsReplyPort(0), "") + assert.False(t, needsReplyPort(dhcpv4.ClientPort), "") +} diff --git a/dhcpv4/bsdp_client.go b/dhcpv4/bsdp/client.go index 5c4dc00..c2e8ae0 100644 --- a/dhcpv4/bsdp_client.go +++ b/dhcpv4/bsdp/client.go @@ -1,25 +1,32 @@ -// +build darwin - -package dhcpv4 +package bsdp import ( "fmt" "net" "syscall" + + "github.com/insomniacslk/dhcp/dhcpv4" ) -// BSDPExchange runs a full BSDP exchange (Inform[list], Ack, Inform[select], +// Client is a BSDP-specific client suitable for performing BSDP exchanges. +type Client dhcpv4.Client + +// Exchange runs a full BSDP exchange (Inform[list], Ack, Inform[select], // Ack). Returns a list of DHCPv4 structures representing the exchange. -func (c *Client) BSDPExchange(ifname string, d *DHCPv4) ([]DHCPv4, error) { - conversation := make([]DHCPv4, 1) +func (c *Client) Exchange(ifname string, informList *dhcpv4.DHCPv4) ([]dhcpv4.DHCPv4, error) { + conversation := make([]dhcpv4.DHCPv4, 1) var err error // INFORM[LIST] - if d == nil { - d, err = NewInformListForInterface(ifname, ClientPort) + if informList == nil { + informList, err = NewInformListForInterface(ifname, dhcpv4.ClientPort) + if err != nil { + return conversation, err + } } - conversation[0] = *d + conversation[0] = *informList + // TODO: deduplicate with code in dhcpv4/client.go fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW) if err != nil { return conversation, err @@ -36,13 +43,15 @@ func (c *Client) BSDPExchange(ifname string, d *DHCPv4) ([]DHCPv4, error) { if err != nil { return conversation, err } - err = BindToInterface(fd, ifname) + err = dhcpv4.BindToInterface(fd, ifname) if err != nil { return conversation, err } - daddr := syscall.SockaddrInet4{Port: ClientPort, Addr: [4]byte{255, 255, 255, 255}} - packet, err := makeRawBroadcastPacket(d.ToBytes()) + bcast := [4]byte{} + copy(bcast[:], net.IPv4bcast) + daddr := syscall.SockaddrInet4{Port: dhcpv4.ClientPort, Addr: bcast} + packet, err := dhcpv4.MakeRawBroadcastPacket(informList.ToBytes()) if err != nil { return conversation, err } @@ -52,16 +61,16 @@ func (c *Client) BSDPExchange(ifname string, d *DHCPv4) ([]DHCPv4, error) { } // ACK 1 - conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: ClientPort}) + conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: dhcpv4.ClientPort}) if err != nil { return conversation, err } defer conn.Close() - buf := make([]byte, maxUDPReceivedPacketSize) + buf := make([]byte, dhcpv4.MaxUDPReceivedPacketSize) oobdata := []byte{} // ignoring oob data n, _, _, _, err := conn.ReadMsgUDP(buf, oobdata) - ack1, err := FromBytes(buf[:n]) + ack1, err := dhcpv4.FromBytes(buf[:n]) if err != nil { return conversation, err } @@ -71,7 +80,6 @@ func (c *Client) BSDPExchange(ifname string, d *DHCPv4) ([]DHCPv4, error) { // Parse boot images sent back by server bootImages, err := ParseBootImageListFromAck(*ack1) - fmt.Println(bootImages) if err != nil { return conversation, err } @@ -80,12 +88,12 @@ func (c *Client) BSDPExchange(ifname string, d *DHCPv4) ([]DHCPv4, error) { } // INFORM[SELECT] - request, err := InformSelectForAck(*ack1, ClientPort, bootImages[0]) + informSelect, err := InformSelectForAck(*ack1, dhcpv4.ClientPort, bootImages[0]) if err != nil { return conversation, err } - conversation = append(conversation, *request) - packet, err = makeRawBroadcastPacket(request.ToBytes()) + conversation = append(conversation, *informSelect) + packet, err = dhcpv4.MakeRawBroadcastPacket(informSelect.ToBytes()) if err != nil { return conversation, err } @@ -95,15 +103,15 @@ func (c *Client) BSDPExchange(ifname string, d *DHCPv4) ([]DHCPv4, error) { } // ACK 2 - buf = make([]byte, maxUDPReceivedPacketSize) + buf = make([]byte, dhcpv4.MaxUDPReceivedPacketSize) n, _, _, _, err = conn.ReadMsgUDP(buf, oobdata) - acknowledge, err := FromBytes(buf[:n]) + ack2, err := dhcpv4.FromBytes(buf[:n]) if err != nil { return conversation, err } // TODO match the packet content // TODO check that the peer address matches the declared server IP and port - conversation = append(conversation, *acknowledge) + conversation = append(conversation, *ack2) return conversation, nil } diff --git a/dhcpv4/bsdp/types.go b/dhcpv4/bsdp/types.go new file mode 100644 index 0000000..54f38e2 --- /dev/null +++ b/dhcpv4/bsdp/types.go @@ -0,0 +1,68 @@ +package bsdp + +import "github.com/insomniacslk/dhcp/dhcpv4" + +// Options (occur as sub-options of DHCP option 43). +const ( + OptionMessageType dhcpv4.OptionCode = iota + 1 + OptionVersion + OptionServerIdentifier + OptionServerPriority + OptionReplyPort + OptionBootImageListPath // Not used + OptionDefaultBootImageID + OptionSelectedBootImageID + OptionBootImageList + OptionNetboot1_0Firmware + OptionBootImageAttributesFilterList + OptionShadowMountPath dhcpv4.OptionCode = 128 + OptionShadowFilePath dhcpv4.OptionCode = 129 + 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{ + OptionMessageType: " Message Type", + OptionVersion: " Version", + OptionServerIdentifier: " Server Identifier", + OptionServerPriority: " Server Priority", + OptionReplyPort: " Reply Port", + OptionBootImageListPath: "", // Not used + OptionDefaultBootImageID: " Default Boot Image ID", + OptionSelectedBootImageID: " Selected Boot Image ID", + OptionBootImageList: " Boot Image List", + OptionNetboot1_0Firmware: " Netboot 1.0 Firmware", + OptionBootImageAttributesFilterList: " Boot Image Attributes Filter List", + OptionShadowMountPath: " Shadow Mount Path", + OptionShadowFilePath: " Shadow File Path", + OptionMachineName: " Machine Name", +} diff --git a/dhcpv4/bsdp_test.go b/dhcpv4/bsdp_test.go deleted file mode 100644 index 80da518..0000000 --- a/dhcpv4/bsdp_test.go +++ /dev/null @@ -1,297 +0,0 @@ -package dhcpv4 - -import ( - "bytes" - "testing" -) - -/* - * BootImageID - */ -func TestBootImageIDToBytes(t *testing.T) { - b := BootImageID{ - isInstall: true, - imageKind: BSDPBootImageMacOSX, - index: 0x1000, - } - actual := b.toBytes() - expected := []byte{0x81, 0, 0x10, 0} - if !bytes.Equal(actual, expected) { - t.Fatalf("Invalid bytes conversion: expected %v, got %v", expected, actual) - } - - b.isInstall = false - actual = b.toBytes() - expected = []byte{0x01, 0, 0x10, 0} - if !bytes.Equal(actual, expected) { - t.Fatalf("Invalid bytes conversion: expected %v, got %v", expected, actual) - } -} - -func TestBootImageIDFromBytes(t *testing.T) { - b := BootImageID{ - isInstall: false, - imageKind: BSDPBootImageMacOSX, - index: 0x1000, - } - newBootImage := bootImageIDFromBytes(b.toBytes()) - if b != newBootImage { - t.Fatalf("Difference in BootImageIDs: expected %v, got %v", b, newBootImage) - } - - b = BootImageID{ - isInstall: true, - imageKind: BSDPBootImageMacOSX, - index: 0x1011, - } - newBootImage = bootImageIDFromBytes(b.toBytes()) - if b != newBootImage { - t.Fatalf("Difference in BootImageIDs: expected %v, got %v", b, newBootImage) - } -} - -/* - * BootImage - */ -func TestBootImageToBytes(t *testing.T) { - b := BootImage{ - ID: BootImageID{ - isInstall: true, - imageKind: BSDPBootImageMacOSX, - 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() - if !bytes.Equal(expected, actual) { - t.Fatalf("Invalid bytes conversion: expected %v, got %v", expected, actual) - } - - b = BootImage{ - ID: BootImageID{ - isInstall: false, - imageKind: BSDPBootImageMacOSX, - 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() - if !bytes.Equal(expected, actual) { - t.Fatalf("Invalid bytes conversion: expected %v, got %v", 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, read, err := bootImageFromBytes(input) - AssertNil(t, err, "error while marshalling BootImage") - AssertEqual(t, read, len(input), "number of bytes from input") - expectedBootImage := BootImage{ - ID: BootImageID{ - isInstall: false, - imageKind: BSDPBootImageMacOSX, - index: 0x1010, - }, - Name: "bsdp-21", - } - AssertEqual(t, *b, expectedBootImage, "invalid marshalling of BootImage") -} - -func TestBootImageFromBytesOnlyBootImageID(t *testing.T) { - // Only a BootImageID, nothing else. - input := []byte{0x1, 0, 0x10, 0x10} - _, _, err := bootImageFromBytes(input) - AssertNotNil(t, err, "short bytestream") -} - -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 (intentially off-by-one) - } - _, _, err := bootImageFromBytes(input) - AssertNotNil(t, err, "short bytestream") -} - -func TestParseBootImageSingleBootImage(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 - } - bs, err := parseBootImagesFromBSDPOption(input) - AssertNil(t, err, "parsing boot image") - AssertEqual(t, len(bs), 1, "length of boot images") - b := bs[0] - expectedBootImage := BootImageID{ - isInstall: false, - imageKind: BSDPBootImageMacOSX, - index: 0x1010, - } - AssertEqual(t, b.ID, expectedBootImage, "boot image ID") - AssertEqual(t, b.Name, "bsdp-21", "boot image name") -} - -func TestParseBootImageMultipleBootImage(t *testing.T) { - input := []byte{ - // boot image 1 - 0x1, 0, 0x10, 0x10, // boot image ID - 7, // len(Name) - 98, 115, 100, 112, 45, 50, 49, // byte-encoding of Name - - // boot image 2 - 0x82, 0, 0x11, 0x22, // boot image ID - 8, // len(Name) - 98, 115, 100, 112, 45, 50, 50, 50, // byte-encoding of Name - } - bs, err := parseBootImagesFromBSDPOption(input) - AssertNil(t, err, "parsing boot image") - AssertEqual(t, len(bs), 2, "length of boot images") - b1 := bs[0] - b2 := bs[1] - expectedID1 := BootImageID{ - isInstall: false, - imageKind: BSDPBootImageMacOSX, - index: 0x1010, - } - expectedID2 := BootImageID{ - isInstall: true, - imageKind: BSDPBootImageMacOSXServer, - index: 0x1122, - } - AssertEqual(t, b1.ID, expectedID1, "boot image ID 1") - AssertEqual(t, b2.ID, expectedID2, "boot image ID 2") - AssertEqual(t, b1.Name, "bsdp-21", "boot image 1 name") - AssertEqual(t, b2.Name, "bsdp-222", "boot image 1 name") -} - -func TestParseBootImageFail(t *testing.T) { - _, err := parseBootImagesFromBSDPOption([]byte{}) - AssertNotNil(t, err, "parseBootImages with empty arg") - - _, err = parseBootImagesFromBSDPOption([]byte{1, 2, 3}) - AssertNotNil(t, err, "parseBootImages with short arg") -} - -/* - * parseVendorOptionsFromOptions - */ -func TestParseVendorOptions(t *testing.T) { - recvOpts := []Option{ - Option{ - Code: OptionDHCPMessageType, - Data: []byte{MessageTypeAck}, - }, - Option{ - Code: OptionBroadcastAddress, - Data: []byte{0xff, 0xff, 0xff, 0xff}, - }, - Option{ - Code: OptionVendorSpecificInformation, - Data: OptionsToBytes([]Option{ - Option{ - Code: BSDPOptionMessageType, - Data: []byte{BSDPMessageTypeList}, - }, - Option{ - Code: BSDPOptionVersion, - Data: BSDPVersion1_0, - }, - }), - }, - } - opts := parseVendorOptionsFromOptions(recvOpts) - AssertEqual(t, len(opts), 2, "len of vendor opts") -} - -func TestParseVendorOptionsFromOptionsNotPresent(t *testing.T) { - recvOpts := []Option{ - Option{ - Code: OptionDHCPMessageType, - Data: []byte{MessageTypeAck}, - }, - Option{ - Code: OptionBroadcastAddress, - Data: []byte{0xff, 0xff, 0xff, 0xff}, - }, - } - opts := parseVendorOptionsFromOptions(recvOpts) - AssertEqual(t, len(opts), 0, "len of vendor opts") -} - -func TestParseVendorOptionsFromOptionsEmpty(t *testing.T) { - options := parseVendorOptionsFromOptions([]Option{}) - AssertEqual(t, len(options), 0, "size of options") -} - -/* - * ParseBootImageListFromAck - */ -func TestParseBootImageListFromAck(t *testing.T) { - bootImages := []BootImage{ - BootImage{ - ID: BootImageID{ - isInstall: true, - imageKind: BSDPBootImageMacOSX, - index: 0x1010, - }, - Name: "bsdp-1", - }, - BootImage{ - ID: BootImageID{ - isInstall: false, - imageKind: BSDPBootImageMacOS9, - index: 0x1111, - }, - Name: "bsdp-2", - }, - } - var bootImageBytes []byte - for _, image := range bootImages { - bootImageBytes = append(bootImageBytes, image.toBytes()...) - } - ack := DHCPv4{ - options: []Option{ - Option{ - Code: OptionVendorSpecificInformation, - Data: OptionsToBytes([]Option{ - Option{ - Code: BSDPOptionBootImageList, - Data: bootImageBytes, - }, - }), - }, - }, - } - - images, err := ParseBootImageListFromAck(ack) - AssertNil(t, err, "error from ParseBootImageListFromAck") - AssertNotNil(t, images, "parsed boot images from ack") - if len(images) != len(bootImages) { - t.Fatalf("Expected same number of BootImages, got %d instead", len(images)) - } - for i := range images { - if images[i] != bootImages[i] { - t.Fatalf("Expected boot images to be same. %v != %v", images[i], bootImages[i]) - } - } -} - -/* - * NewInformListForInterface - */ diff --git a/dhcpv4/client.go b/dhcpv4/client.go index ef2af4e..3d91b84 100644 --- a/dhcpv4/client.go +++ b/dhcpv4/client.go @@ -10,7 +10,7 @@ import ( ) const ( - maxUDPReceivedPacketSize = 8192 // arbitrary size. Theoretically could be up to 65kb + MaxUDPReceivedPacketSize = 8192 // arbitrary size. Theoretically could be up to 65kb ) type Client struct { @@ -19,7 +19,7 @@ type Client struct { Timeout time.Duration } -func makeRawBroadcastPacket(payload []byte) ([]byte, error) { +func MakeRawBroadcastPacket(payload []byte) ([]byte, error) { udp := make([]byte, 8) binary.BigEndian.PutUint16(udp[:2], ClientPort) binary.BigEndian.PutUint16(udp[2:4], ServerPort) @@ -82,7 +82,7 @@ func (c *Client) Exchange(ifname string, d *DHCPv4) ([]DHCPv4, error) { } daddr := syscall.SockaddrInet4{Port: ClientPort, Addr: [4]byte{255, 255, 255, 255}} - packet, err := makeRawBroadcastPacket(d.ToBytes()) + packet, err := MakeRawBroadcastPacket(d.ToBytes()) if err != nil { return conversation, err } @@ -98,7 +98,7 @@ func (c *Client) Exchange(ifname string, d *DHCPv4) ([]DHCPv4, error) { } defer conn.Close() - buf := make([]byte, maxUDPReceivedPacketSize) + buf := make([]byte, MaxUDPReceivedPacketSize) oobdata := []byte{} // ignoring oob data n, _, _, _, err := conn.ReadMsgUDP(buf, oobdata) offer, err := FromBytes(buf[:n]) @@ -115,7 +115,7 @@ func (c *Client) Exchange(ifname string, d *DHCPv4) ([]DHCPv4, error) { return conversation, err } conversation = append(conversation, *request) - packet, err = makeRawBroadcastPacket(request.ToBytes()) + packet, err = MakeRawBroadcastPacket(request.ToBytes()) if err != nil { return conversation, err } @@ -125,7 +125,7 @@ func (c *Client) Exchange(ifname string, d *DHCPv4) ([]DHCPv4, error) { } // Acknowledge - buf = make([]byte, maxUDPReceivedPacketSize) + buf = make([]byte, MaxUDPReceivedPacketSize) n, _, _, _, err = conn.ReadMsgUDP(buf, oobdata) acknowledge, err := FromBytes(buf[:n]) if err != nil { diff --git a/dhcpv4/dhcpv4.go b/dhcpv4/dhcpv4.go index f0690b2..182d4a4 100644 --- a/dhcpv4/dhcpv4.go +++ b/dhcpv4/dhcpv4.go @@ -38,11 +38,13 @@ type DHCPv4 struct { options []Option } -// interfaceV4Addr obtains the currently-configured IPv4 address for iface. -func interfaceV4Addr(iface *net.Interface) (net.IP, error) { +// IPv4AddrsForInterface obtains the currently-configured, non-loopback IPv4 +// addresses for iface. +func IPv4AddrsForInterface(iface *net.Interface) ([]net.IP, error) { addrs, err := iface.Addrs() + var v4addrs []net.IP if err != nil { - return nil, err + return v4addrs, err } for _, addr := range addrs { var ip net.IP @@ -60,9 +62,9 @@ func interfaceV4Addr(iface *net.Interface) (net.IP, error) { if ip == nil { continue } - return ip, nil + v4addrs = append(v4addrs, ip) } - return nil, errors.New("Could not get local IPv4 address") + return v4addrs, nil } // GenerateTransactionID generates a random 32-bits number suitable for use as @@ -105,7 +107,7 @@ func New() (*DHCPv4, error) { copy(d.clientHwAddr[:], []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) copy(d.serverHostName[:], []byte{}) copy(d.bootFileName[:], []byte{}) - options, err := OptionsFromBytesWithMagicCookie(MagicCookie) + options, err := OptionsFromBytes(MagicCookie) if err != nil { return nil, err } @@ -133,7 +135,7 @@ func NewDiscoveryForInterface(ifname string) (*DHCPv4, error) { d.SetBroadcast() d.AddOption(Option{ Code: OptionDHCPMessageType, - Data: []byte{MessageTypeDiscover}, + Data: []byte{byte(MessageTypeDiscover)}, }) d.AddOption(Option{ Code: OptionParameterRequestList, @@ -170,15 +172,15 @@ func NewInformForInterface(ifname string, needsBroadcast bool) (*DHCPv4, error) } // Set Client IP as iface's currently-configured IP. - localIP, err := interfaceV4Addr(iface) - if err != nil { - return nil, err + localIPs, err := IPv4AddrsForInterface(iface) + if err != nil || len(localIPs) == 0 { + return nil, fmt.Errorf("could not get local IPs for iface %s", ifname) } - d.SetClientIPAddr(localIP) + d.SetClientIPAddr(localIPs[0]) d.AddOption(Option{ Code: OptionDHCPMessageType, - Data: []byte{MessageTypeInform}, + Data: []byte{byte(MessageTypeInform)}, }) return d, nil @@ -215,7 +217,7 @@ func RequestFromOffer(offer DHCPv4) (*DHCPv4, error) { d.SetServerIPAddr(serverIP) d.AddOption(Option{ Code: OptionDHCPMessageType, - Data: []byte{MessageTypeRequest}, + Data: []byte{byte(MessageTypeRequest)}, }) d.AddOption(Option{ Code: OptionRequestedIPAddress, @@ -252,7 +254,7 @@ func FromBytes(data []byte) (*DHCPv4, error) { copy(d.clientHwAddr[:], data[28:44]) copy(d.serverHostName[:], data[44:108]) copy(d.bootFileName[:], data[108:236]) - options, err := OptionsFromBytesWithMagicCookie(data[236:]) + options, err := OptionsFromBytes(data[236:]) if err != nil { return nil, err } @@ -568,18 +570,6 @@ func (d *DHCPv4) Summary() string { ) ret += " options=\n" for _, opt := range d.options { - // Parse and display sub-options - if opt.Code == OptionVendorSpecificInformation { - ret += fmt.Sprintf(" %v ->\n", OptionCodeToString[OptionVendorSpecificInformation]) - subopts, err := OptionsFromBytes(opt.Data) - if err == nil { - for _, subopt := range subopts { - ret += fmt.Sprintf(" %v\n", subopt.BSDPString()) - } - continue - } - // fall-through to normal display if the above fails - } ret += fmt.Sprintf(" %v\n", opt.String()) if opt.Code == OptionEnd { break @@ -638,6 +628,6 @@ func (d *DHCPv4) ToBytes() []byte { ret = append(ret, d.serverHostName[:64]...) ret = append(ret, d.bootFileName[:128]...) d.ValidateOptions() // print warnings about broken options, if any - ret = append(ret, OptionsToBytesWithMagicCookie(d.options)...) + ret = append(ret, OptionsToBytes(d.options)...) return ret } diff --git a/dhcpv4/dhcpv4_test.go b/dhcpv4/dhcpv4_test.go index a3a1955..0d569c8 100644 --- a/dhcpv4/dhcpv4_test.go +++ b/dhcpv4/dhcpv4_test.go @@ -17,29 +17,6 @@ func AssertEqual(t *testing.T, a, b interface{}, what string) { } } -func AssertNotNil(t *testing.T, a interface{}, what string) { - if a == nil { - t.Fatalf("Expected %s to not be nil. %v", what, a) - } -} - -func AssertNil(t *testing.T, a interface{}, what string) { - if a != nil { - t.Fatalf("Expected %s to be nil. %v", what, a) - } -} - -func AssertEqualSlice(t *testing.T, a, b []interface{}, what string) { - if len(a) != len(b) { - t.Fatalf("Invalid %s. %v != %v", what, a, b) - } - for i := range a { - if a[i] != b[i] { - t.Fatalf("Invalid %s. %v != %v at index %d", what, a, b, i) - } - } -} - func AssertEqualBytes(t *testing.T, a, b []byte, what string) { if !bytes.Equal(a, b) { t.Fatalf("Invalid %s. %v != %v", what, a, b) diff --git a/dhcpv4/options.go b/dhcpv4/options.go index 989a93b..6576c05 100644 --- a/dhcpv4/options.go +++ b/dhcpv4/options.go @@ -10,6 +10,7 @@ type OptionCode byte var MagicCookie = []byte{99, 130, 83, 99} +// TODO: implement Option as an interface similar to dhcpv6. type Option struct { Code OptionCode Data []byte @@ -38,27 +39,27 @@ func ParseOption(dataStart []byte) (*Option, error) { } } -// OptionsFromBytesWithMagicCookie parses a sequence of bytes until the end and -// builds a list of options from it. The sequence must contain the Magic Cookie. -// Returns an error if any invalid option or length is found. -func OptionsFromBytesWithMagicCookie(data []byte) ([]Option, error) { +// OptionsFromBytes parses a sequence of bytes until the end and builds a list +// of options from it. The sequence must contain the Magic Cookie. Returns an +// error if any invalid option or length is found. +func OptionsFromBytes(data []byte) ([]Option, error) { if len(data) < len(MagicCookie) { return nil, errors.New("Invalid options: shorter than 4 bytes") } if !bytes.Equal(data[:len(MagicCookie)], MagicCookie) { return nil, fmt.Errorf("Invalid Magic Cookie: %v", data[:len(MagicCookie)]) } - opts, err := OptionsFromBytes(data[len(MagicCookie):]) + opts, err := OptionsFromBytesWithoutMagicCookie(data[len(MagicCookie):]) if err != nil { return nil, err } return opts, nil } -// OptionsFromBytes parses a sequence of bytes until the end and builds a list -// of options from it. The sequence should not contain the DHCP magic cookie. -// Returns an error if any invalid option or length is found. -func OptionsFromBytes(data []byte) ([]Option, error) { +// OptionsFromBytesWithoutMagicCookie parses a sequence of bytes until the end +// and builds a list of options from it. The sequence should not contain the +// DHCP magic cookie. Returns an error if any invalid option or length is found. +func OptionsFromBytesWithoutMagicCookie(data []byte) ([]Option, error) { options := make([]Option, 0, 10) idx := 0 for { @@ -86,15 +87,15 @@ func OptionsFromBytes(data []byte) ([]Option, error) { return options, nil } -// OptionsToBytesWithMagicCookie converts a list of options to a wire-format -// representation with the DHCP magic cookie prepended. -func OptionsToBytesWithMagicCookie(options []Option) []byte { - ret := MagicCookie - return append(ret, OptionsToBytes(options)...) +// OptionsToBytes converts a list of options to a wire-format representation +// with the DHCP magic cookie prepended. +func OptionsToBytes(options []Option) []byte { + return append(MagicCookie, OptionsToBytesWithoutMagicCookie(options)...) } -// OptionsToBytes converts a list of options to a wire-format representation. -func OptionsToBytes(options []Option) []byte { +// OptionsToBytesWithoutMagicCookie converts a list of options to a wire-format +// representation. +func OptionsToBytesWithoutMagicCookie(options []Option) []byte { ret := []byte{} for _, opt := range options { ret = append(ret, opt.ToBytes()...) @@ -110,16 +111,6 @@ func (o *Option) String() string { return fmt.Sprintf("%v -> %v", code, o.Data) } -// BSDPString converts a BSDP-specific option embedded in -// vendor-specific information to a human-readable string. -func (o *Option) BSDPString() string { - code, ok := BSDPOptionCodeToString[o.Code] - if !ok { - code = "Unknown" - } - return fmt.Sprintf("%v -> %v", code, o.Data) -} - func (o *Option) ToBytes() []byte { // Convert a single option to its wire-format representation ret := []byte{byte(o.Code)} diff --git a/dhcpv4/options_test.go b/dhcpv4/options_test.go index 47258aa..07be9bf 100644 --- a/dhcpv4/options_test.go +++ b/dhcpv4/options_test.go @@ -56,19 +56,19 @@ func TestOptionsFromBytes(t *testing.T) { 255, // end 0, 0, 0, //padding } - opts, err := OptionsFromBytesWithMagicCookie(options) + opts, err := OptionsFromBytes(options) if err != nil { t.Fatal(err) } // each padding byte counts as an option. Magic Cookie doesn't add up if len(opts) != 5 { - t.Fatalf("Invalid options length. Expected 5, got %v", len(opts)) + t.Fatal("Invalid options length. Expected 5, got %v", len(opts)) } if opts[0].Code != OptionNameServer { - t.Fatalf("Invalid option code. Expected %v, got %v", OptionNameServer, opts[0].Code) + t.Fatal("Invalid option code. Expected %v, got %v", OptionNameServer, opts[0].Code) } if !bytes.Equal(opts[0].Data, options[6:10]) { - t.Fatalf("Invalid option data. Expected %v, got %v", options[6:10], opts[0].Data) + t.Fatal("Invalid option data. Expected %v, got %v", options[6:10], opts[0].Data) } if opts[1].Code != OptionEnd { t.Fatalf("Invalid option code. Expected %v, got %v", OptionEnd, opts[1].Code) @@ -80,7 +80,7 @@ func TestOptionsFromBytes(t *testing.T) { func TestOptionsFromBytesZeroLength(t *testing.T) { options := []byte{} - _, err := OptionsFromBytesWithMagicCookie(options) + _, err := OptionsFromBytes(options) if err == nil { t.Fatal("Expected an error, got none") } @@ -88,36 +88,36 @@ func TestOptionsFromBytesZeroLength(t *testing.T) { func TestOptionsFromBytesBadMagicCookie(t *testing.T) { options := []byte{1, 2, 3, 4} - _, err := OptionsFromBytesWithMagicCookie(options) + _, err := OptionsFromBytes(options) if err == nil { t.Fatal("Expected an error, got none") } } -func TestOptionsToBytesWithMagicCookie(t *testing.T) { +func TestOptionsToBytes(t *testing.T) { originalOptions := []byte{ 99, 130, 83, 99, // Magic Cookie 5, 4, 192, 168, 1, 1, // DNS 255, // end 0, 0, 0, //padding } - options, err := OptionsFromBytesWithMagicCookie(originalOptions) + options, err := OptionsFromBytes(originalOptions) if err != nil { t.Fatal(err) } - finalOptions := OptionsToBytesWithMagicCookie(options) + finalOptions := OptionsToBytes(options) if !bytes.Equal(originalOptions, finalOptions) { t.Fatalf("Invalid options. Expected %v, got %v", originalOptions, finalOptions) } } -func TestOptionsToBytesWithMagicCookieEmpty(t *testing.T) { +func TestOptionsToBytesEmpty(t *testing.T) { originalOptions := []byte{99, 130, 83, 99} - options, err := OptionsFromBytesWithMagicCookie(originalOptions) + options, err := OptionsFromBytes(originalOptions) if err != nil { t.Fatal(err) } - finalOptions := OptionsToBytesWithMagicCookie(options) + finalOptions := OptionsToBytes(options) if !bytes.Equal(originalOptions, finalOptions) { t.Fatalf("Invalid options. Expected %v, got %v", originalOptions, finalOptions) } @@ -146,21 +146,3 @@ func TestOptionsToStringDHCPMessageType(t *testing.T) { t.Fatalf("Invalid string representation: %v", stropt) } } - -func TestBSDPOptionToString(t *testing.T) { - // Parse message type - option := Option{ - Code: BSDPOptionMessageType, - Data: []byte{BSDPMessageTypeList}, - } - stropt := option.BSDPString() - AssertEqual(t, stropt, "BSDP Message Type -> [1]", "BSDP string representation") - - // Parse failure - option = Option{ - Code: OptionCode(12), // invalid BSDP Opcode - Data: []byte{1, 2, 3}, - } - stropt = option.BSDPString() - AssertEqual(t, stropt, "Unknown -> [1 2 3]", "BSDP string representation") -} diff --git a/dhcpv4/types.go b/dhcpv4/types.go index 4f3ad0f..6ec4d9b 100644 --- a/dhcpv4/types.go +++ b/dhcpv4/types.go @@ -3,9 +3,12 @@ package dhcpv4 // values from http://www.networksorcery.com/enp/protocol/dhcp.htm and // http://www.networksorcery.com/enp/protocol/bootp/options.htm +// MessageType represents the possible DHCP message types - DISCOVER, OFFER, etc +type MessageType byte + // DHCP message types const ( - MessageTypeDiscover byte = iota + 1 + MessageTypeDiscover MessageType = iota + 1 MessageTypeOffer MessageTypeRequest MessageTypeDecline @@ -359,22 +362,3 @@ var OptionCodeToString = map[OptionCode]string{ OptionEnd: "End", } - -// BSDPOptionCodeToString maps BSDP OptionCodes to human-readable strings -// describing what they are. -var BSDPOptionCodeToString = map[OptionCode]string{ - BSDPOptionMessageType: "BSDP Message Type", - BSDPOptionVersion: "BSDP Version", - BSDPOptionServerIdentifier: "BSDP Server Identifier", - BSDPOptionServerPriority: "BSDP Server Priority", - BSDPOptionReplyPort: "BSDP Reply Port", - BSDPOptionBootImageListPath: "", // Not used - BSDPOptionDefaultBootImageID: "BSDP Default Boot Image ID", - BSDPOptionSelectedBootImageID: "BSDP Selected Boot Image ID", - BSDPOptionBootImageList: "BSDP Boot Image List", - BSDPOptionNetboot1_0Firmware: "BSDP Netboot 1.0 Firmware", - BSDPOptionBootImageAttributesFilterList: "BSDP Boot Image Attributes Filter List", - BSDPOptionShadowMountPath: "BSDP Shadow Mount Path", - BSDPOptionShadowFilePath: "BSDP Shadow File Path", - BSDPOptionMachineName: "BSDP Machine Name", -} |