diff options
Diffstat (limited to 'dhcpv4/bsdp.go')
-rw-r--r-- | dhcpv4/bsdp.go | 328 |
1 files changed, 328 insertions, 0 deletions
diff --git a/dhcpv4/bsdp.go b/dhcpv4/bsdp.go new file mode 100644 index 0000000..05542be --- /dev/null +++ b/dhcpv4/bsdp.go @@ -0,0 +1,328 @@ +// +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 +) + +// Versions (seen so far) +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: append([]byte{4}, 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, + }) + } + + d.AddOption(Option{ + Code: OptionDHCPMessageType, + Data: []byte{MessageTypeInform}, + }) + d.AddOption(Option{ + Code: OptionVendorSpecificInformation, + Data: OptionsToBytes(vendorOpts), + }) + d.AddOption(Option{Code: OptionEnd}) + return d, nil +} |