summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorSean Karlage <skarlage@fb.com>2018-02-03 21:55:38 -0800
committerSean Karlage <skarlage@fb.com>2018-03-03 11:25:28 -0800
commit28e9aa6c820ffcb9028422b1179c1326dc76229c (patch)
tree4652daf1f69e8696867c5c325f009922a70d3361
parentac949192ce781902de712ea495b04fc84709ac2e (diff)
Add BSDP support
Adds support for constructing INFORM/ACK messages from Apple's Boot Service Discovery Protocol for netbooting (pxebooting) Apple hardware. The canonical reference for BSDP is: http://opensource.apple.com/source/bootp/bootp-198.1/Documentation/BSDP.doc
-rw-r--r--dhcpv4/bsdp.go328
-rw-r--r--dhcpv4/bsdp_client.go109
-rw-r--r--dhcpv4/bsdp_test.go297
-rw-r--r--dhcpv4/client.go3
-rw-r--r--dhcpv4/dhcpv4.go80
-rw-r--r--dhcpv4/dhcpv4_test.go26
-rw-r--r--dhcpv4/options.go44
-rw-r--r--dhcpv4/options_test.go24
-rw-r--r--dhcpv4/types.go12
9 files changed, 890 insertions, 33 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
+}
diff --git a/dhcpv4/bsdp_client.go b/dhcpv4/bsdp_client.go
new file mode 100644
index 0000000..5c4dc00
--- /dev/null
+++ b/dhcpv4/bsdp_client.go
@@ -0,0 +1,109 @@
+// +build darwin
+
+package dhcpv4
+
+import (
+ "fmt"
+ "net"
+ "syscall"
+)
+
+// BSDPExchange 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)
+ var err error
+
+ // INFORM[LIST]
+ if d == nil {
+ d, err = NewInformListForInterface(ifname, ClientPort)
+ }
+ conversation[0] = *d
+
+ fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
+ if err != nil {
+ return conversation, err
+ }
+ err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
+ if err != nil {
+ return conversation, err
+ }
+ err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1)
+ if err != nil {
+ return conversation, err
+ }
+ err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1)
+ if err != nil {
+ return conversation, err
+ }
+ err = 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())
+ if err != nil {
+ return conversation, err
+ }
+ err = syscall.Sendto(fd, packet, 0, &daddr)
+ if err != nil {
+ return conversation, err
+ }
+
+ // ACK 1
+ conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: ClientPort})
+ if err != nil {
+ return conversation, err
+ }
+ defer conn.Close()
+
+ buf := make([]byte, maxUDPReceivedPacketSize)
+ oobdata := []byte{} // ignoring oob data
+ n, _, _, _, err := conn.ReadMsgUDP(buf, oobdata)
+ ack1, err := 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, *ack1)
+
+ // Parse boot images sent back by server
+ bootImages, err := ParseBootImageListFromAck(*ack1)
+ fmt.Println(bootImages)
+ if err != nil {
+ return conversation, err
+ }
+ if len(bootImages) == 0 {
+ return conversation, fmt.Errorf("Got no BootImages from server")
+ }
+
+ // INFORM[SELECT]
+ request, err := InformSelectForAck(*ack1, ClientPort, bootImages[0])
+ if err != nil {
+ return conversation, err
+ }
+ conversation = append(conversation, *request)
+ packet, err = makeRawBroadcastPacket(request.ToBytes())
+ if err != nil {
+ return conversation, err
+ }
+ err = syscall.Sendto(fd, packet, 0, &daddr)
+ if err != nil {
+ return conversation, err
+ }
+
+ // ACK 2
+ buf = make([]byte, maxUDPReceivedPacketSize)
+ n, _, _, _, err = conn.ReadMsgUDP(buf, oobdata)
+ acknowledge, err := 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)
+
+ return conversation, nil
+}
diff --git a/dhcpv4/bsdp_test.go b/dhcpv4/bsdp_test.go
new file mode 100644
index 0000000..80da518
--- /dev/null
+++ b/dhcpv4/bsdp_test.go
@@ -0,0 +1,297 @@
+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 55f303d..ef2af4e 100644
--- a/dhcpv4/client.go
+++ b/dhcpv4/client.go
@@ -2,10 +2,11 @@ package dhcpv4
import (
"encoding/binary"
- "golang.org/x/net/ipv4"
"net"
"syscall"
"time"
+
+ "golang.org/x/net/ipv4"
)
const (
diff --git a/dhcpv4/dhcpv4.go b/dhcpv4/dhcpv4.go
index 49eeae1..dc691d9 100644
--- a/dhcpv4/dhcpv4.go
+++ b/dhcpv4/dhcpv4.go
@@ -5,10 +5,11 @@ import (
"encoding/binary"
"errors"
"fmt"
- "github.com/insomniacslk/dhcp/iana"
"log"
"net"
"strings"
+
+ "github.com/insomniacslk/dhcp/iana"
)
// HeaderSize is the DHCPv4 header size in bytes.
@@ -37,6 +38,33 @@ type DHCPv4 struct {
options []Option
}
+// interfaceV4Addr obtains the currently-configured IPv4 address for iface.
+func interfaceV4Addr(iface *net.Interface) (net.IP, error) {
+ addrs, err := iface.Addrs()
+ if err != nil {
+ return nil, err
+ }
+ for _, addr := range addrs {
+ var ip net.IP
+ switch v := addr.(type) {
+ case *net.IPAddr:
+ ip = v.IP
+ case *net.IPNet:
+ ip = v.IP
+ }
+
+ if ip == nil || ip.IsLoopback() {
+ continue
+ }
+ ip = ip.To4()
+ if ip == nil {
+ continue
+ }
+ return ip, nil
+ }
+ return nil, errors.New("Could not get local IPv4 address")
+}
+
// GenerateTransactionID generates a random 32-bits number suitable for use as
// TransactionID
func GenerateTransactionID() (*uint32, error) {
@@ -77,7 +105,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 := OptionsFromBytes(MagicCookie)
+ options, err := OptionsFromBytesWithMagicCookie(MagicCookie)
if err != nil {
return nil, err
}
@@ -105,7 +133,7 @@ func NewDiscoveryForInterface(ifname string) (*DHCPv4, error) {
d.SetBroadcast()
d.AddOption(Option{
Code: OptionDHCPMessageType,
- Data: []byte{1},
+ Data: []byte{MessageTypeDiscover},
})
d.AddOption(Option{
Code: OptionParameterRequestList,
@@ -116,6 +144,46 @@ func NewDiscoveryForInterface(ifname string) (*DHCPv4, error) {
return d, nil
}
+// NewInformForInterface builds a new DHCPv4 Informational message with default
+// Ethernet HW type and the hardware address obtained from the specified
+// interface. It does NOT put a DHCP End option at the end.
+func NewInformForInterface(ifname string, needsBroadcast bool) (*DHCPv4, error) {
+ d, err := New()
+ if err != nil {
+ return nil, err
+ }
+
+ // get hw addr
+ iface, err := net.InterfaceByName(ifname)
+ if err != nil {
+ return nil, err
+ }
+ d.SetOpcode(OpcodeBootRequest)
+ d.SetHwType(iana.HwTypeEthernet)
+ d.SetHwAddrLen(uint8(len(iface.HardwareAddr)))
+ d.SetClientHwAddr(iface.HardwareAddr)
+
+ if needsBroadcast {
+ d.SetBroadcast()
+ } else {
+ d.SetUnicast()
+ }
+
+ // Set Client IP as iface's currently-configured IP.
+ localIP, err := interfaceV4Addr(iface)
+ if err != nil {
+ return nil, err
+ }
+ d.SetClientIPAddr(localIP)
+
+ d.AddOption(Option{
+ Code: OptionDHCPMessageType,
+ Data: []byte{MessageTypeInform},
+ })
+
+ return d, nil
+}
+
// RequestFromOffer builds a DHCPv4 request from an offer.
func RequestFromOffer(offer DHCPv4) (*DHCPv4, error) {
d, err := New()
@@ -147,7 +215,7 @@ func RequestFromOffer(offer DHCPv4) (*DHCPv4, error) {
d.SetServerIPAddr(serverIP)
d.AddOption(Option{
Code: OptionDHCPMessageType,
- Data: []byte{3},
+ Data: []byte{MessageTypeRequest},
})
d.AddOption(Option{
Code: OptionRequestedIPAddress,
@@ -184,7 +252,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 := OptionsFromBytes(data[236:])
+ options, err := OptionsFromBytesWithMagicCookie(data[236:])
if err != nil {
return nil, err
}
@@ -558,6 +626,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, OptionsToBytes(d.options)...)
+ ret = append(ret, OptionsToBytesWithMagicCookie(d.options)...)
return ret
}
diff --git a/dhcpv4/dhcpv4_test.go b/dhcpv4/dhcpv4_test.go
index 5a12207..a3a1955 100644
--- a/dhcpv4/dhcpv4_test.go
+++ b/dhcpv4/dhcpv4_test.go
@@ -2,9 +2,10 @@ package dhcpv4
import (
"bytes"
- "github.com/insomniacslk/dhcp/iana"
"net"
"testing"
+
+ "github.com/insomniacslk/dhcp/iana"
)
// NOTE: if one of the following Assert* fails where expected and got values are
@@ -16,6 +17,29 @@ 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 59f2c83..aa9808b 100644
--- a/dhcpv4/options.go
+++ b/dhcpv4/options.go
@@ -38,18 +38,29 @@ func ParseOption(dataStart []byte) (*Option, error) {
}
}
-func OptionsFromBytes(data []byte) ([]Option, error) {
- // Parse a sequence of bytes until the end and build a list of options from
- // it. The sequence must contain the Magic Cookie.
- // Returns an error if any invalid option or length is found.
- if len(data) < 4 {
+// 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) {
+ if len(data) < len(MagicCookie) {
return nil, errors.New("Invalid options: shorter than 4 bytes")
}
- if !bytes.Equal(data[:4], MagicCookie) {
- return nil, errors.New(fmt.Sprintf("Invalid Magic Cookie: %v", data[:4]))
+ if !bytes.Equal(data[:len(MagicCookie)], MagicCookie) {
+ return nil, fmt.Errorf("Invalid Magic Cookie: %v", data[:len(MagicCookie)])
+ }
+ opts, err := OptionsFromBytes(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) {
options := make([]Option, 0, 10)
- idx := 4
+ idx := 0
for {
if idx == len(data) {
break
@@ -64,9 +75,10 @@ func OptionsFromBytes(data []byte) ([]Option, error) {
return nil, err
}
options = append(options, *opt)
+
+ // Options with zero length have no length byte, so here we handle the
+ // ones with nonzero length
if len(opt.Data) > 0 {
- // options with zero length have no length byte, so here we handle the ones with
- // nonzero length
idx++
}
idx += len(opt.Data)
@@ -74,10 +86,16 @@ func OptionsFromBytes(data []byte) ([]Option, error) {
return options, nil
}
-func OptionsToBytes(options []Option) []byte {
- // Convert a list of options to a wire-format representation. This will
- // include the Magic Cookie
+// 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.
+func OptionsToBytes(options []Option) []byte {
+ ret := []byte{}
for _, opt := range options {
ret = append(ret, opt.ToBytes()...)
}
diff --git a/dhcpv4/options_test.go b/dhcpv4/options_test.go
index 07be9bf..5a34ed5 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 := OptionsFromBytes(options)
+ opts, err := OptionsFromBytesWithMagicCookie(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.Fatal("Invalid options length. Expected 5, got %v", len(opts))
+ t.Fatalf("Invalid options length. Expected 5, got %v", len(opts))
}
if opts[0].Code != OptionNameServer {
- t.Fatal("Invalid option code. Expected %v, got %v", OptionNameServer, opts[0].Code)
+ t.Fatalf("Invalid option code. Expected %v, got %v", OptionNameServer, opts[0].Code)
}
if !bytes.Equal(opts[0].Data, options[6:10]) {
- t.Fatal("Invalid option data. Expected %v, got %v", options[6:10], opts[0].Data)
+ t.Fatalf("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 := OptionsFromBytes(options)
+ _, err := OptionsFromBytesWithMagicCookie(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 := OptionsFromBytes(options)
+ _, err := OptionsFromBytesWithMagicCookie(options)
if err == nil {
t.Fatal("Expected an error, got none")
}
}
-func TestOptionsToBytes(t *testing.T) {
+func TestOptionsToBytesWithMagicCookie(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 := OptionsFromBytes(originalOptions)
+ options, err := OptionsFromBytesWithMagicCookie(originalOptions)
if err != nil {
t.Fatal(err)
}
- finalOptions := OptionsToBytes(options)
+ finalOptions := OptionsToBytesWithMagicCookie(options)
if !bytes.Equal(originalOptions, finalOptions) {
t.Fatalf("Invalid options. Expected %v, got %v", originalOptions, finalOptions)
}
}
-func TestOptionsToBytesEmpty(t *testing.T) {
+func TestOptionsToBytesWithMagicCookieEmpty(t *testing.T) {
originalOptions := []byte{99, 130, 83, 99}
- options, err := OptionsFromBytes(originalOptions)
+ options, err := OptionsFromBytesWithMagicCookie(originalOptions)
if err != nil {
t.Fatal(err)
}
- finalOptions := OptionsToBytes(options)
+ finalOptions := OptionsToBytesWithMagicCookie(options)
if !bytes.Equal(originalOptions, finalOptions) {
t.Fatalf("Invalid options. Expected %v, got %v", originalOptions, finalOptions)
}
diff --git a/dhcpv4/types.go b/dhcpv4/types.go
index e8a71f5..f8d29bd 100644
--- a/dhcpv4/types.go
+++ b/dhcpv4/types.go
@@ -3,6 +3,18 @@ package dhcpv4
// values from http://www.networksorcery.com/enp/protocol/dhcp.htm and
// http://www.networksorcery.com/enp/protocol/bootp/options.htm
+// DHCP message types
+const (
+ MessageTypeDiscover byte = iota + 1
+ MessageTypeOffer
+ MessageTypeRequest
+ MessageTypeDecline
+ MessageTypeAck
+ MessageTypeNak
+ MessageTypeRelease
+ MessageTypeInform
+)
+
// OpcodeType represents a DHCPv4 opcode.
type OpcodeType uint8