diff options
author | Sean Karlage <skarlage@fb.com> | 2018-02-03 21:55:38 -0800 |
---|---|---|
committer | Sean Karlage <skarlage@fb.com> | 2018-03-03 11:25:28 -0800 |
commit | 28e9aa6c820ffcb9028422b1179c1326dc76229c (patch) | |
tree | 4652daf1f69e8696867c5c325f009922a70d3361 /dhcpv4 | |
parent | ac949192ce781902de712ea495b04fc84709ac2e (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
Diffstat (limited to 'dhcpv4')
-rw-r--r-- | dhcpv4/bsdp.go | 328 | ||||
-rw-r--r-- | dhcpv4/bsdp_client.go | 109 | ||||
-rw-r--r-- | dhcpv4/bsdp_test.go | 297 | ||||
-rw-r--r-- | dhcpv4/client.go | 3 | ||||
-rw-r--r-- | dhcpv4/dhcpv4.go | 80 | ||||
-rw-r--r-- | dhcpv4/dhcpv4_test.go | 26 | ||||
-rw-r--r-- | dhcpv4/options.go | 44 | ||||
-rw-r--r-- | dhcpv4/options_test.go | 24 | ||||
-rw-r--r-- | dhcpv4/types.go | 12 |
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 |