summaryrefslogtreecommitdiffhomepage
path: root/dhcpv4/bsdp/bsdp.go
diff options
context:
space:
mode:
Diffstat (limited to 'dhcpv4/bsdp/bsdp.go')
-rw-r--r--dhcpv4/bsdp/bsdp.go336
1 files changed, 336 insertions, 0 deletions
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
+}