blob: bdccb6596ec0c903498cc596dd60b7695691438c [file] [log] [blame]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package network provides general CrOS network goodies.
package network
import (
"bytes"
"encoding/binary"
"fmt"
"math/rand"
"net"
"strings"
"chromiumos/tast/errors"
)
// option represents an option in a DHCP packet. Options may or may not be
// present in any given packet, depending on the configurations of the client
// and the server. Below, we'll provide different implementations of
// option to reflect that different kinds of options serialize to on the wire
// formats in different ways.
type option interface {
pack(interface{}) ([]byte, error)
unpack([]byte) (interface{}, error)
name() string
number() uint8
}
// optionBase stores the name and number fields of a given option.
type optionBase struct {
nameField string // human readable name for this option.
numberField uint8 // unique identifier for this option.
}
func (o optionBase) name() string {
return o.nameField
}
func (o optionBase) number() uint8 {
return o.numberField
}
// appendOption serializes the option and appends the serialized bytes to the
// given byte slice.
func appendOption(buf []byte, o option, val interface{}) ([]byte, error) {
serializedValue, err := o.pack(val)
if err != nil {
return nil, err
}
buf = append(buf, o.number(), uint8(len(serializedValue)))
return append(buf, serializedValue...), nil
}
type byteOption struct {
optionBase
}
func (o byteOption) pack(value interface{}) ([]byte, error) {
valInt, ok := value.(uint8)
if !ok {
return nil, errors.New("expected uint8")
}
return []byte{valInt}, nil
}
func (o byteOption) unpack(buf []byte) (interface{}, error) {
if len(buf) != 1 {
return nil, errors.New("expected 1 byte")
}
return uint8(buf[0]), nil
}
type shortOption struct {
optionBase
}
func (o shortOption) pack(value interface{}) ([]byte, error) {
valInt, ok := value.(uint16)
if !ok {
return nil, errors.New("expected uint16")
}
buf := make([]byte, 2)
binary.BigEndian.PutUint16(buf, valInt)
return buf, nil
}
func (o shortOption) unpack(buf []byte) (interface{}, error) {
if len(buf) != 2 {
return nil, errors.New("expected 2 bytes")
}
return binary.BigEndian.Uint16(buf), nil
}
type intOption struct {
optionBase
}
func (o intOption) pack(value interface{}) ([]byte, error) {
valInt, ok := value.(uint32)
if !ok {
return nil, errors.New("expected uint32")
}
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, valInt)
return buf, nil
}
func (o intOption) unpack(buf []byte) (interface{}, error) {
if len(buf) != 4 {
return nil, errors.New("expected 4 bytes")
}
return binary.BigEndian.Uint32(buf), nil
}
type ipAddressOption struct {
optionBase
}
func ipToBytes(ipAddr string) ([]byte, error) {
ip := net.ParseIP(ipAddr)
if ip == nil {
return nil, errors.Errorf("unable to parse IP: %q", ipAddr)
}
if ip.To4() == nil {
return nil, errors.New("expected IPv4 address")
}
return ip.To4(), nil
}
func bytesToIP(buf []byte) (string, error) {
if len(buf) != 4 {
return "", errors.New("not the expected length of an IPv4 address")
}
ip := net.IP(buf)
return ip.String(), nil
}
func (o ipAddressOption) pack(value interface{}) ([]byte, error) {
valStr, ok := value.(string)
if !ok {
return nil, errors.New("expected string")
}
return ipToBytes(valStr)
}
func (o ipAddressOption) unpack(buf []byte) (interface{}, error) {
return bytesToIP(buf)
}
type ipListOption struct {
optionBase
}
func (o ipListOption) pack(value interface{}) ([]byte, error) {
valSlice, ok := value.([]string)
if !ok {
return nil, errors.New("expected string slice")
}
var buf []byte
for _, addr := range valSlice {
ipBytes, err := ipToBytes(addr)
if err != nil {
return nil, err
}
buf = append(buf, ipBytes...)
}
return buf, nil
}
func (o ipListOption) unpack(buf []byte) (interface{}, error) {
if len(buf)%4 != 0 {
return nil, errors.Errorf("%d is not a multiple of 4", len(buf))
}
var ipList []string
for len(buf) >= 4 {
ipString, err := bytesToIP(buf[:4])
if err != nil {
return nil, err
}
ipList = append(ipList, ipString)
buf = buf[4:]
}
return ipList, nil
}
type rawOption struct {
optionBase
}
func (o rawOption) pack(value interface{}) ([]byte, error) {
valStr, ok := value.([]byte)
if !ok {
return nil, errors.New("expected byte slice")
}
return valStr, nil
}
func (o rawOption) unpack(buf []byte) (interface{}, error) {
return buf, nil
}
// classlessStaticRoutesOption is a RFC 3442 compliant classless static route
// option parser and serializer. The symbolic "value" packed and unpacked from
// this class is a slice of staticRoutes (defined below).
type classlessStaticRoutesOption struct {
optionBase
}
type staticRoute struct {
prefixSize uint8
destAddr string
routerAddr string
}
func (o classlessStaticRoutesOption) pack(value interface{}) ([]byte, error) {
routeList, ok := value.([]staticRoute)
if !ok {
return nil, errors.New("expected staticRoute slice")
}
var buf []byte
for _, route := range routeList {
buf = append(buf, route.prefixSize)
destAddrCount := (route.prefixSize + 7) / 8
destAddrBytes, err := ipToBytes(route.destAddr)
if err != nil {
return nil, err
}
buf = append(buf, destAddrBytes[:destAddrCount]...)
routerAddrBytes, err := ipToBytes(route.routerAddr)
if err != nil {
return nil, err
}
buf = append(buf, routerAddrBytes...)
}
return buf, nil
}
func (o classlessStaticRoutesOption) unpack(buf []byte) (interface{}, error) {
var routeList []staticRoute
for len(buf) > 0 {
prefixSize := int(buf[0])
buf = buf[1:]
destAddrCount := (prefixSize + 7) / 8
if destAddrCount+4 > len(buf) {
return nil, errors.New("classless domain list is corrupted")
}
destAddrBytes := make([]byte, 4)
copy(destAddrBytes, buf[:destAddrCount])
destAddr, err := bytesToIP(destAddrBytes)
buf = buf[destAddrCount:]
if err != nil {
return nil, err
}
routerAddrBytes := make([]byte, 4)
copy(routerAddrBytes, buf[:4])
routerAddr, err := bytesToIP(routerAddrBytes)
buf = buf[4:]
if err != nil {
return nil, err
}
routeList = append(routeList, staticRoute{uint8(prefixSize), destAddr, routerAddr})
}
return routeList, nil
}
// domainListOption is a RFC 1035 compliant domain list option parser and
// serializer.
// There are some clever compression optimizations that it does not implement
// for serialization, but correctly parses. This should be sufficient for
// testing.
type domainListOption struct {
optionBase
}
func (o domainListOption) pack(value interface{}) ([]byte, error) {
domainList, ok := value.([]string)
if !ok {
return nil, errors.New("expected string slice")
}
var buf []byte
for _, domain := range domainList {
for _, part := range strings.Split(domain, ".") {
if len(part) >= 256 {
return nil, errors.Errorf("len(part) = %d, expected length less than 256", len(part))
}
buf = append(buf, uint8(len(part)))
buf = append(buf, part...)
}
buf = append(buf, uint8(0))
}
return buf, nil
}
func (o domainListOption) unpack(buf []byte) (interface{}, error) {
var domainList []string
offset := 0
for offset < len(buf) {
newOffset, domainParts, err := readDomainName(buf, offset)
if err != nil {
return nil, err
}
domainName := strings.Join(domainParts, ".")
domainList = append(domainList, domainName)
if newOffset <= offset {
return nil, errors.New("parsing logic error is letting domain list parsing go on forever")
}
offset = newOffset
}
return domainList, nil
}
// Various RFC's let you finish a domain name by pointing to an existing domain
// name rather than repeating the same suffix. All such pointers are two buf
// long, specify the offset in the byte string, and begin with pointerPrefix
// to distinguish them from normal characters.
const pointerPrefix = '\xC0'
// readDomainName recursively parses a domain name from a domain name list.
func readDomainName(buf []byte, offset int) (int, []string, error) {
var parts []string
for {
if offset >= len(buf) {
return 0, nil, errors.New("domain list ended without a NULL byte")
}
maybePartLen := int(buf[offset])
offset++
if maybePartLen == 0 {
return offset, parts, nil
} else if (maybePartLen & pointerPrefix) == pointerPrefix {
if offset >= len(buf) {
return 0, nil, errors.New("missing second byte of domain suffix pointer")
}
maybePartLen &= ^pointerPrefix
pointerOffset := ((maybePartLen << 8) + int(buf[offset]))
offset++
_, moreParts, err := readDomainName(buf, pointerOffset)
if err != nil {
return 0, nil, err
}
parts = append(parts, moreParts...)
return offset, parts, nil
} else {
partLen := maybePartLen
if offset+partLen >= len(buf) {
return 0, nil, errors.New("part of a domain goes beyond data length")
}
parts = append(parts, string(buf[offset:offset+partLen]))
offset += partLen
}
}
}
// field represents a required field in a DHCP packet. Similar to
// option, we'll implement this interface to reflect that different
// fields serialize toon the wire formats in different ways.
type field interface {
pack(interface{}) ([]byte, error)
unpack([]byte) (interface{}, error)
name() string
offset() int
size() int
}
type fieldBase struct {
nameField string // human readable name for this field.
offsetField int // defines the starting byte of the field in the binary packet string.
sizeField int // defines the fixed size that must be respected
}
func appendField(buf []byte, f field, val interface{}) ([]byte, error) {
buf = append(buf, make([]byte, f.offset()-len(buf))...)
serializedValue, err := f.pack(val)
if err != nil {
return nil, err
}
return append(buf, serializedValue...), nil
}
func (f fieldBase) name() string {
return f.nameField
}
func (f fieldBase) offset() int {
return f.offsetField
}
func (f fieldBase) size() int {
return f.sizeField
}
type byteField struct {
fieldBase
}
func (f byteField) pack(value interface{}) ([]byte, error) {
valInt, ok := value.(uint8)
if !ok {
return nil, errors.New("expected uint8")
}
return []byte{valInt}, nil
}
func (f byteField) unpack(buf []byte) (interface{}, error) {
if len(buf) != 1 {
return nil, errors.New("expected 1 byte")
}
return uint8(buf[0]), nil
}
type shortField struct {
fieldBase
}
func (f shortField) pack(value interface{}) ([]byte, error) {
valInt, ok := value.(uint16)
if !ok {
return nil, errors.New("expected uint16")
}
buf := make([]byte, 2)
binary.BigEndian.PutUint16(buf, valInt)
return buf, nil
}
func (f shortField) unpack(buf []byte) (interface{}, error) {
if len(buf) != 2 {
return nil, errors.New("expected 2 bytes")
}
return binary.BigEndian.Uint16(buf), nil
}
type intField struct {
fieldBase
}
func (f intField) pack(value interface{}) ([]byte, error) {
valInt, ok := value.(uint32)
if !ok {
return nil, errors.New("expected uint32")
}
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, valInt)
return buf, nil
}
func (f intField) unpack(buf []byte) (interface{}, error) {
if len(buf) != 4 {
return nil, errors.New("expected 4 bytes")
}
return binary.BigEndian.Uint32(buf), nil
}
type hwAddrField struct {
fieldBase
}
func (f hwAddrField) pack(value interface{}) ([]byte, error) {
valBytes, ok := value.([]byte)
if !ok {
return nil, errors.New("expected byte slice")
} else if len(valBytes) > 16 {
return nil, errors.New("expected byte slice of length no more than 16")
}
return valBytes, nil
}
func (f hwAddrField) unpack(buf []byte) (interface{}, error) {
if len(buf) != 16 {
return nil, errors.New("expected byte slice of length 16")
}
return buf, nil
}
type serverNameField struct {
fieldBase
}
func (f serverNameField) pack(value interface{}) ([]byte, error) {
valStr, ok := value.(string)
if !ok {
return nil, errors.New("expected string")
} else if len(valStr) > 64 {
return nil, errors.New("expected string of length no more than 64")
}
buf := make([]byte, 64)
copy(buf, valStr)
return buf, nil
}
func (f serverNameField) unpack(buf []byte) (interface{}, error) {
if len(buf) != 64 {
return nil, errors.New("expected byte slice of length 64")
}
return strings.Trim(string(buf), "\x00"), nil
}
type bootFileField struct {
fieldBase
}
func (f bootFileField) pack(value interface{}) ([]byte, error) {
valStr, ok := value.(string)
if !ok {
return nil, errors.New("expected string")
} else if len(valStr) > 128 {
return nil, errors.New("expected string of length no more than 128")
}
buf := make([]byte, 128)
copy(buf, valStr)
return buf, nil
}
func (f bootFileField) unpack(buf []byte) (interface{}, error) {
if len(buf) != 128 {
return nil, errors.New("expected byte slice of length 128")
}
return strings.Trim(string(buf), "\x00"), nil
}
type ipAddressField struct {
fieldBase
}
func (f ipAddressField) pack(value interface{}) ([]byte, error) {
valStr, ok := value.(string)
if !ok {
return nil, errors.New("expected string")
}
return ipToBytes(valStr)
}
func (f ipAddressField) unpack(buf []byte) (interface{}, error) {
return bytesToIP(buf)
}
// DHCP fields.
var (
// These are required in every DHCP packet. Without these fields, the packet
// will not even pass dhcpPacket.isValid
op = byteField{fieldBase{"op", 0, 1}}
hwType = byteField{fieldBase{"htype", 1, 1}}
hwAddrLen = byteField{fieldBase{"hlen", 2, 1}}
relayHops = byteField{fieldBase{"hops", 3, 1}}
transactionID = intField{fieldBase{"xid", 4, 4}}
timeSinceStart = shortField{fieldBase{"secs", 8, 2}}
flags = shortField{fieldBase{"flags", 10, 2}}
clientIP = ipAddressField{fieldBase{"ciaddr", 12, 4}}
yourIP = ipAddressField{fieldBase{"yiaddr", 16, 4}}
serverIP = ipAddressField{fieldBase{"siaddr", 20, 4}}
gatewayIP = ipAddressField{fieldBase{"giaddr", 24, 4}}
clientHWAddr = hwAddrField{fieldBase{"chaddr", 28, 16}}
// The following two fields are considered "legacy BOOTP" fields but may
// sometimes be used by DHCP clients.
legacyServerName = serverNameField{fieldBase{"servername", 44, 64}}
legacyBootFile = bootFileField{fieldBase{"bootfile", 108, 128}}
magicCookie = intField{fieldBase{"magic_cookie", 236, 4}}
)
// DHCP options.
var (
timeOffset = intOption{optionBase{"time_offset", 2}}
routers = ipListOption{optionBase{"routers", 3}}
subnetMask = ipAddressOption{optionBase{"subnet_mask", 1}}
timeServers = ipListOption{optionBase{"time_servers", 4}}
nameServers = ipListOption{optionBase{"name_servers", 5}}
dnsServers = ipListOption{optionBase{"dns_servers", 6}}
logServers = ipListOption{optionBase{"log_servers", 7}}
cookieServers = ipListOption{optionBase{"cookie_servers", 8}}
lprServers = ipListOption{optionBase{"lpr_servers", 9}}
impressServers = ipListOption{optionBase{"impress_servers", 10}}
resourceLOCServers = ipListOption{optionBase{"resource_loc_servers", 11}}
hostName = rawOption{optionBase{"host_name", 12}}
bootFileSize = shortOption{optionBase{"boot_file_size", 13}}
meritDumpFile = rawOption{optionBase{"merit_dump_file", 14}}
domainName = rawOption{optionBase{"domain_name", 15}}
swapServer = ipAddressOption{optionBase{"swap_server", 16}}
rootPath = rawOption{optionBase{"root_path", 17}}
extensions = rawOption{optionBase{"extensions", 18}}
interfaceMTU = shortOption{optionBase{"interface_mtu", 26}}
vendorEncapsulatedOptions = rawOption{optionBase{"vendor_encapsulated_options", 43}}
requestedIP = ipAddressOption{optionBase{"requested_ip", 50}}
ipLeaseTime = intOption{optionBase{"ip_lease_time", 51}}
optionOverload = byteOption{optionBase{"option_overload", 52}}
dhcpMessageType = byteOption{optionBase{"dhcp_message_type", 53}}
serverID = ipAddressOption{optionBase{"server_id", 54}}
parameterRequestList = rawOption{optionBase{"parameter_request_list", 55}}
message = rawOption{optionBase{"message", 56}}
maxDHCPMessageSize = shortOption{optionBase{"max_dhcp_message_size", 57}}
renewalT1TimeValue = intOption{optionBase{"renewal_t1_time_value", 58}}
rebindingT2TimeValue = intOption{optionBase{"rebinding_t2_time_value", 59}}
vendorID = rawOption{optionBase{"vendor_id", 60}}
clientID = rawOption{optionBase{"client_id", 61}}
tftpServerName = rawOption{optionBase{"tftp_server_name", 66}}
bootfileName = rawOption{optionBase{"bootfile_name", 67}}
fullyQualifiedDomainName = rawOption{optionBase{"fqdn", 81}}
dnsDomainSearchList = domainListOption{optionBase{"domain_search_list", 119}}
classlessStaticRoutes = classlessStaticRoutesOption{optionBase{"classless_static_routes", 121}}
webProxyAutoDiscovery = rawOption{optionBase{"wpad", 252}}
)
type msgType struct {
name string
optionVal uint8
}
// From RFC2132, the valid DHCP message types are as follows.
var (
unknown = msgType{"UNKNOWN", 0}
discovery = msgType{"DISCOVERY", 1}
offer = msgType{"OFFER", 2}
request = msgType{"REQUEST", 3}
decline = msgType{"DECLINE", 4}
ack = msgType{"ACK", 5}
nak = msgType{"NAK", 6}
release = msgType{"RELEASE", 7}
inform = msgType{"INFORM", 8}
)
const (
// This is per RFC 2131. The wording doesn't seem to say that the packets
// must be this big, but that has been the historic assumption in
// implementations.
minPacketSize = 300
ipv4Null = "0.0.0.0"
)
// Option constants.
const (
// Unlike every other option the pad and end options are just single bytes
// "\x00" and "\xff" (without length or data fields).
optionPad = 0
optionEnd = 255
optionsStartOffset = 240
)
// Field values.
const (
// The op field in an IPv4 packet is either 1 or 2 depending on whether the
// packet is from a server or from a client.
opClientRequest = uint8(1)
opServerResponse = uint8(2)
// 1 == 10mb ethernet hardware address type (aka MAC).
hwType10MBEth = uint8(1)
// MAC addresses are still 6 bytes long.
hwAddrLen10MBEth = uint8(6)
magicCookieVal = uint32(0x63825363)
)
var (
commonFields = []field{
op,
hwType,
hwAddrLen,
relayHops,
transactionID,
timeSinceStart,
flags,
clientIP,
yourIP,
serverIP,
gatewayIP,
clientHWAddr,
}
requiredFields = append(append([]field(nil), commonFields...), magicCookie)
allFields = append(append([]field(nil), commonFields...), []field{legacyServerName, legacyBootFile, magicCookie}...)
// allOptions are possible options that may not be in every packet.
// Frequently, the client can include a bunch of options that indicate that it
// would like to receive information about time servers, routers, lpr servers,
// and much more, but the DHCP server can usually ignore those requests.
//
// Eventually, each option is encoded as:
// <option.number(), option.size(), [slice of option.size() bytes]>
// Unlike fields, which make up a fixed packet format, options can be in any
// order, except where they cannot. For instance, option 1 must follow option
// 3 if both are supplied. For this reason, potential options are in this
// list, and added to the packet in this order every time.
//
// size < 0 indicates that this is variable length field of at least
// abs(length) bytes in size.
allOptions = []option{
timeOffset,
routers,
subnetMask,
timeServers,
nameServers,
dnsServers,
logServers,
cookieServers,
lprServers,
impressServers,
resourceLOCServers,
hostName,
bootFileSize,
meritDumpFile,
swapServer,
domainName,
rootPath,
extensions,
interfaceMTU,
vendorEncapsulatedOptions,
requestedIP,
ipLeaseTime,
optionOverload,
dhcpMessageType,
serverID,
parameterRequestList,
message,
maxDHCPMessageSize,
renewalT1TimeValue,
rebindingT2TimeValue,
vendorID,
clientID,
tftpServerName,
bootfileName,
fullyQualifiedDomainName,
dnsDomainSearchList,
classlessStaticRoutes,
webProxyAutoDiscovery,
}
msgTypeByNum = []msgType{
unknown,
discovery,
offer,
request,
decline,
ack,
nak,
release,
inform,
}
defaultParameterRequestList = []uint8{
requestedIP.number(),
ipLeaseTime.number(),
serverID.number(),
subnetMask.number(),
routers.number(),
dnsServers.number(),
hostName.number(),
}
)
func getDHCPOptionByNumber(number uint8) option {
for _, option := range allOptions {
if option.number() == number {
return option
}
}
return nil
}
type optionMap map[option]interface{}
type fieldMap map[field]interface{}
// dhcpPacket is a class that represents a single DHCP packet and contains some
// logic to create and parse binary strings containing on the wire DHCP packets.
// While you could call |newDHCPPacket| explicitly, most users should use the
// factories to construct packets with reasonable default values in most of
// the fields, even if those values are zeros.
type dhcpPacket struct {
options optionMap
fields fieldMap
}
// createDiscovery creates a discovery packet.
// Fill in fields of a DHCP packet as if it were being sent from macAddr.
// Requests subnet masks, broadcast addresses, router addresses, DNS addresses,
// domain search lists, client host name, and NTP server addresses. Note that
// the offer packet received in response to this packet will probably not
// contain all of that information.
func createDiscovery(macAddr []byte) (*dhcpPacket, error) {
// MAC addresses are actually only 6 bytes long, however, for whatever reason,
// DHCP allocated 12 bytes to this field. Ease the burden on developers and
// hide this detail.
macAddr = append(append([]byte{}, macAddr...), bytes.Repeat([]byte{optionPad}, 12-len(macAddr))...)
packet, err := newDHCPPacket(nil)
if err != nil {
return nil, err
}
packet.setField(op, opClientRequest)
packet.setField(hwType, hwType10MBEth)
packet.setField(hwAddrLen, hwAddrLen10MBEth)
packet.setField(relayHops, uint8(0))
packet.setField(transactionID, rand.Uint32())
packet.setField(timeSinceStart, uint16(0))
packet.setField(flags, uint16(0))
packet.setField(clientIP, ipv4Null)
packet.setField(yourIP, ipv4Null)
packet.setField(serverIP, ipv4Null)
packet.setField(gatewayIP, ipv4Null)
packet.setField(clientHWAddr, macAddr)
packet.setField(magicCookie, magicCookieVal)
packet.setOption(dhcpMessageType, discovery.optionVal)
return packet, nil
}
// createOffer creates an offer packet, given some fields that tie the
// packet to a particular offer.
func createOffer(txnID uint32, macAddr []byte, offerIP, svrIP string) (*dhcpPacket, error) {
packet, err := newDHCPPacket(nil)
if err != nil {
return nil, err
}
packet.setField(op, opServerResponse)
packet.setField(hwType, hwType10MBEth)
packet.setField(hwAddrLen, hwAddrLen10MBEth)
packet.setField(relayHops, uint8(0))
packet.setField(transactionID, txnID)
packet.setField(timeSinceStart, uint16(0))
packet.setField(flags, uint16(0))
packet.setField(clientIP, ipv4Null)
packet.setField(yourIP, offerIP)
packet.setField(serverIP, svrIP)
packet.setField(gatewayIP, ipv4Null)
packet.setField(clientHWAddr, macAddr)
packet.setField(magicCookie, magicCookieVal)
packet.setOption(dhcpMessageType, offer.optionVal)
return packet, nil
}
func createRequest(txnID uint32, macAddr []byte) (*dhcpPacket, error) {
packet, err := newDHCPPacket(nil)
if err != nil {
return nil, err
}
packet.setField(op, opClientRequest)
packet.setField(hwType, hwType10MBEth)
packet.setField(hwAddrLen, hwAddrLen10MBEth)
packet.setField(relayHops, uint8(0))
packet.setField(transactionID, txnID)
packet.setField(timeSinceStart, uint16(0))
packet.setField(flags, uint16(0))
packet.setField(clientIP, ipv4Null)
packet.setField(yourIP, ipv4Null)
packet.setField(serverIP, ipv4Null)
packet.setField(gatewayIP, ipv4Null)
packet.setField(clientHWAddr, macAddr)
packet.setField(magicCookie, magicCookieVal)
packet.setOption(dhcpMessageType, request.optionVal)
return packet, nil
}
func createAck(txnID uint32, macAddr []byte, grantedIP, svrIP string) (*dhcpPacket, error) {
packet, err := newDHCPPacket(nil)
if err != nil {
return nil, err
}
packet.setField(op, opServerResponse)
packet.setField(hwType, hwType10MBEth)
packet.setField(hwAddrLen, hwAddrLen10MBEth)
packet.setField(relayHops, uint8(0))
packet.setField(transactionID, txnID)
packet.setField(timeSinceStart, uint16(0))
packet.setField(flags, uint16(0))
packet.setField(clientIP, ipv4Null)
packet.setField(yourIP, grantedIP)
packet.setField(serverIP, svrIP)
packet.setField(gatewayIP, ipv4Null)
packet.setField(clientHWAddr, macAddr)
packet.setField(magicCookie, magicCookieVal)
packet.setOption(dhcpMessageType, ack.optionVal)
return packet, nil
}
func createNAK(txnID uint32, macAddr []byte) (*dhcpPacket, error) {
packet, err := newDHCPPacket(nil)
if err != nil {
return nil, err
}
packet.setField(op, opServerResponse)
packet.setField(hwType, hwType10MBEth)
packet.setField(hwAddrLen, hwAddrLen10MBEth)
packet.setField(relayHops, uint8(0))
packet.setField(transactionID, txnID)
packet.setField(timeSinceStart, uint16(0))
packet.setField(flags, uint16(0))
packet.setField(clientIP, ipv4Null)
packet.setField(yourIP, ipv4Null)
packet.setField(serverIP, ipv4Null)
packet.setField(gatewayIP, ipv4Null)
packet.setField(clientHWAddr, macAddr)
packet.setField(magicCookie, magicCookieVal)
packet.setOption(dhcpMessageType, nak.optionVal)
return packet, nil
}
// newDHCPPacket creates a dhcpPacket, filling in fields from a byte string if
// given.
// Assumes that the packet starts at offset 0 in the binary string. This
// includes the fields and options. Fields are different from options in that we
// bother to decode these into more usable data types like integers rather than
// keeping them as raw byte strings. Fields are also required to exist, unlike
// options which may not.
// Each option is encoded as a tuple <option number, length, data> where option
// number is a byte indicating the type of option, length indicates the number
// of bytes in the data for option, and data is a length array of bytes. The
// only exceptions to this rule are the 0 and 255 options, which have 0 data
// length, and no length byte. These tuples are then simply appended to each
// other. This encoding is the same as the BOOTP vendor extension field
// encoding.
func newDHCPPacket(buf []byte) (*dhcpPacket, error) {
var packet dhcpPacket
packet.options = make(optionMap)
packet.fields = make(fieldMap)
if len(buf) == 0 {
return &packet, nil
}
if len(buf) < optionsStartOffset+1 {
return nil, errors.New("invalid byte string for packet")
}
for _, field := range allFields {
fieldVal, err := field.unpack(buf[field.offset() : field.offset()+field.size()])
if err != nil {
return nil, err
}
packet.fields[field] = fieldVal
}
offset := optionsStartOffset
var domainSearchListByteString []byte
for offset < len(buf) && buf[offset] != optionEnd {
dataType := buf[offset]
offset++
if dataType == optionPad {
continue
}
dataLength := int(buf[offset])
offset++
data := buf[offset : offset+dataLength]
offset += dataLength
option := getDHCPOptionByNumber(dataType)
if option == nil {
continue
}
if option == dnsDomainSearchList {
// In a cruel twist of fate, the server is allowed to give multiple
// options with this number. The client is expected to concatenate the
// byte strings together and use it as a single value.
domainSearchListByteString = append(domainSearchListByteString, data...)
continue
}
optionVal, err := (option).unpack(data)
if err != nil {
return nil, err
}
packet.options[option] = optionVal
}
if len(domainSearchListByteString) > 0 {
domainSearchListVal, err := dnsDomainSearchList.unpack(domainSearchListByteString)
if err != nil {
return nil, err
}
packet.options[dnsDomainSearchList] = domainSearchListVal
}
return &packet, nil
}
func (d *dhcpPacket) clientHWAddr() ([]byte, error) {
addr, ok := d.fields[clientHWAddr]
if !ok {
return nil, errors.New("client addr field not found")
}
addrBytes, ok := addr.([]byte)
if !ok {
return nil, errors.New("expected byte slice type")
}
return addrBytes, nil
}
// isValid checks that we have (at a minimum) values for all the required
// fields, and that the magic cookie is set correctly.
func (d *dhcpPacket) isValid() error {
for _, field := range requiredFields {
if d.fields[field] == nil {
return errors.Errorf("required field %s is missing from DHCP packet", field.name())
}
}
if d.fields[magicCookie] != magicCookieVal {
return errors.Errorf("magic cookie value is %x, expected %x", d.fields[magicCookie], magicCookieVal)
}
return nil
}
// msgType gets the value of the DHCP Message Type option in this packet.
// If the option is not present, or the value of the option is not recognized,
// returns msgTypeUnknown.
func (d *dhcpPacket) msgType() (msgType, error) {
typeNum, ok := d.options[dhcpMessageType]
if !ok {
return unknown, errors.New("message type option not found")
}
typeNumInt, ok := typeNum.(uint8)
if !ok {
return unknown, errors.New("expected uint8 type")
}
if typeNumInt > 0 && int(typeNumInt) < len(msgTypeByNum) {
return msgTypeByNum[typeNumInt], nil
}
return unknown, errors.New("invalid message type")
}
func (d *dhcpPacket) txnID() (uint32, error) {
ID, ok := d.fields[transactionID]
if !ok {
return 0, errors.New("transaction ID field not found")
}
IDInt, ok := ID.(uint32)
if !ok {
return 0, errors.New("expected uint32 type")
}
return IDInt, nil
}
func (d *dhcpPacket) field(field field) interface{} {
return d.fields[field]
}
func (d *dhcpPacket) option(option option) interface{} {
return d.options[option]
}
func (d *dhcpPacket) setField(field field, fieldValue interface{}) {
d.fields[field] = fieldValue
}
func (d *dhcpPacket) setOption(option option, optionValue interface{}) {
d.options[option] = optionValue
}
func (d *dhcpPacket) marshal() ([]byte, error) {
if err := d.isValid(); err != nil {
return nil, errors.Wrap(err, "invalid packet")
}
var data []byte
var err error
for _, field := range allFields {
fieldVal, ok := d.fields[field]
if !ok {
continue
}
data, err = appendField(data, field, fieldVal)
if err != nil {
return nil, err
}
}
for _, option := range allOptions {
optionVal, ok := d.options[option]
if !ok {
continue
}
data, err = appendOption(data, option, optionVal)
if err != nil {
return nil, err
}
}
data = append(data, optionEnd)
return append(data, bytes.Repeat([]byte{optionPad}, minPacketSize-len(data))...), nil
}
func (d *dhcpPacket) String() string {
var options, fields []string
for field, fieldVal := range d.fields {
fieldStr := fmt.Sprintf("%v=%v", field.name(), fieldVal)
fields = append(fields, fieldStr)
}
for option, optionVal := range d.options {
optionStr := fmt.Sprintf("%v=%v", option.name(), optionVal)
options = append(options, optionStr)
}
return fmt.Sprintf("<DHCPPacket fields=[%s], options=[%s]>", strings.Join(fields, ","), strings.Join(options, ","))
}