mirror of
https://github.com/danderson/netboot.git
synced 2025-10-16 10:01:20 +02:00
Initial implementation of DHCP parsing.
This commit is contained in:
parent
90c89c2bd7
commit
133baa88bd
270
dhcp/dhcp.go
Normal file
270
dhcp/dhcp.go
Normal file
@ -0,0 +1,270 @@
|
||||
// Package dhcp implements parsing and serialization of DHCP packets.
|
||||
package dhcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
var magic = []byte{99, 130, 83, 99}
|
||||
|
||||
// MessageType is the type of a DHCP message.
|
||||
type MessageType int
|
||||
|
||||
// Message types as described in RFC 2131.
|
||||
const (
|
||||
MsgDiscover MessageType = iota + 1
|
||||
MsgOffer
|
||||
MsgRequest
|
||||
MsgDecline
|
||||
MsgAck
|
||||
MsgNack
|
||||
MsgRelease
|
||||
MsgInform
|
||||
)
|
||||
|
||||
func (mt MessageType) String() string {
|
||||
switch mt {
|
||||
case MsgDiscover:
|
||||
return "DHCPDISCOVER"
|
||||
case MsgOffer:
|
||||
return "DHCPOFFER"
|
||||
case MsgRequest:
|
||||
return "DHCPREQUEST"
|
||||
case MsgDecline:
|
||||
return "DHCPDECLINE"
|
||||
case MsgAck:
|
||||
return "DHCPACK"
|
||||
case MsgNack:
|
||||
return "DHCPNAK"
|
||||
case MsgRelease:
|
||||
return "DHCPRELEASE"
|
||||
case MsgInform:
|
||||
return "DHCPINFORM"
|
||||
default:
|
||||
return fmt.Sprintf("<unknown DHCP message type %d>", mt)
|
||||
}
|
||||
}
|
||||
|
||||
// Packet represents a DHCP packet.
|
||||
type Packet struct {
|
||||
Type MessageType
|
||||
TransactionID string
|
||||
Broadcast bool
|
||||
HardwareAddr net.HardwareAddr // Only ethernet supported at the moment
|
||||
|
||||
ClientAddr net.IP // Client's current IP address (it will respond to ARP for this IP)
|
||||
YourAddr net.IP // Client IP address offered/assigned by server
|
||||
RelayAddr net.IP // IP address of DHCP relay agent, if an agent forwarded the request
|
||||
|
||||
BootServerAddr net.IP // "Next bootstrap server" IP address, used for netbooting
|
||||
BootServerName string
|
||||
BootFilename string
|
||||
|
||||
Options Options
|
||||
}
|
||||
|
||||
// Marshal returns the wire encoding of p.
|
||||
func (p *Packet) Marshal() ([]byte, error) {
|
||||
if len(p.TransactionID) != 4 {
|
||||
return nil, errors.New("transaction ID must be 4 bytes")
|
||||
}
|
||||
if len(p.HardwareAddr) != 6 {
|
||||
return nil, errors.New("non-ethernet hardware address not supported")
|
||||
}
|
||||
if len(p.BootServerName) > 64 {
|
||||
return nil, errors.New("sname must be <= 64 bytes")
|
||||
}
|
||||
optsInFile, optsInSname := false, false
|
||||
v, ok := p.Options.Byte(52)
|
||||
if ok {
|
||||
optsInFile, optsInFile = v&1 != 0, v&2 != 0
|
||||
}
|
||||
if optsInFile && p.BootFilename != "" {
|
||||
return nil, errors.New("DHCP option 52 says to use the 'file' field for options, but BootFilename is not empty")
|
||||
}
|
||||
if optsInSname && p.BootServerName != "" {
|
||||
return nil, errors.New("DHCP option 52 says to use the 'sname' field for options, but BootServerName is not empty")
|
||||
}
|
||||
|
||||
ret := new(bytes.Buffer)
|
||||
ret.Grow(244)
|
||||
|
||||
switch p.Type {
|
||||
case MsgDiscover, MsgRequest, MsgDecline, MsgRelease, MsgInform:
|
||||
ret.WriteByte(1)
|
||||
case MsgOffer, MsgAck, MsgNack:
|
||||
ret.WriteByte(2)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown DHCP message type %d", p.Type)
|
||||
}
|
||||
// Hardware address type = Ethernet
|
||||
ret.WriteByte(1)
|
||||
// Hardware address length = 6
|
||||
ret.WriteByte(6)
|
||||
// Hops = 0
|
||||
ret.WriteByte(0)
|
||||
// Transaction ID
|
||||
ret.WriteString(p.TransactionID)
|
||||
// Seconds elapsed
|
||||
ret.Write([]byte{0, 0})
|
||||
// Broadcast flag
|
||||
if p.Broadcast {
|
||||
ret.Write([]byte{0x80, 0})
|
||||
} else {
|
||||
ret.Write([]byte{0, 0})
|
||||
}
|
||||
|
||||
writeIP(ret, p.ClientAddr)
|
||||
writeIP(ret, p.YourAddr)
|
||||
writeIP(ret, p.BootServerAddr)
|
||||
writeIP(ret, p.RelayAddr)
|
||||
|
||||
// MAC address + 10 bytes of padding
|
||||
ret.Write([]byte(p.HardwareAddr))
|
||||
ret.Write([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
|
||||
|
||||
opts := p.Options
|
||||
var err error
|
||||
if optsInSname {
|
||||
opts, err = opts.marshalLimited(ret, 64, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
ret.WriteString(p.BootServerName)
|
||||
for i := len(p.BootServerName); i < 64; i++ {
|
||||
ret.WriteByte(0)
|
||||
}
|
||||
}
|
||||
|
||||
if optsInFile {
|
||||
opts, err = opts.marshalLimited(ret, 128, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
ret.WriteString(p.BootServerName)
|
||||
for i := len(p.BootServerName); i < 64; i++ {
|
||||
ret.WriteByte(0)
|
||||
}
|
||||
}
|
||||
|
||||
ret.Write(magic)
|
||||
if err := opts.MarshalTo(ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret.Bytes(), nil
|
||||
}
|
||||
|
||||
func writeIP(w io.Writer, ip net.IP) {
|
||||
ip = ip.To4()
|
||||
if ip == nil {
|
||||
w.Write([]byte{0, 0, 0, 0})
|
||||
} else {
|
||||
w.Write([]byte(ip))
|
||||
}
|
||||
}
|
||||
|
||||
// Unmarshal parses a DHCP message and returns a Packet.
|
||||
func Unmarshal(bs []byte) (*Packet, error) {
|
||||
// 244 bytes is the minimum size of a valid DHCP message:
|
||||
// - BOOTP header (236b)
|
||||
// - DHCP magic (4b)
|
||||
// - DHCP message type mandatory option (3b)
|
||||
// - End of options marker (1b)
|
||||
if len(bs) < 244 {
|
||||
return nil, errors.New("packet too short")
|
||||
}
|
||||
|
||||
if !bytes.Equal(bs[236:240], magic) {
|
||||
return nil, errors.New("packet does not have DHCP magic number")
|
||||
}
|
||||
|
||||
ret := &Packet{
|
||||
Options: make(Options),
|
||||
}
|
||||
|
||||
if bs[1] != 1 || bs[2] != 6 {
|
||||
return nil, fmt.Errorf("packet has unsupported hardware address type/length %d/%d", bs[1], bs[2])
|
||||
}
|
||||
ret.HardwareAddr = net.HardwareAddr(bs[28:34])
|
||||
ret.TransactionID = string(bs[4:8])
|
||||
if binary.BigEndian.Uint16(bs[10:12])&0x8000 != 0 {
|
||||
ret.Broadcast = true
|
||||
}
|
||||
|
||||
ret.ClientAddr = net.IP(bs[12:16])
|
||||
ret.YourAddr = net.IP(bs[16:20])
|
||||
ret.BootServerAddr = net.IP(bs[20:24])
|
||||
ret.RelayAddr = net.IP(bs[24:28])
|
||||
|
||||
if err := ret.Options.Unmarshal(bs[240:]); err != nil {
|
||||
return nil, fmt.Errorf("packet has malformed options section: %s", err)
|
||||
}
|
||||
|
||||
// The 'file' and 'sname' BOOTP fields can either have the obvious
|
||||
// meaning from BOOTP, or can store extra DHCP options if the main
|
||||
// options section specifies the "Option overload" option.
|
||||
file, sname := false, false
|
||||
v, ok := ret.Options.Byte(52)
|
||||
if ok {
|
||||
file, sname = v&1 != 0, v&2 != 0
|
||||
}
|
||||
if sname {
|
||||
if err := ret.Options.Unmarshal(bs[44:108]); err != nil {
|
||||
return nil, fmt.Errorf("packet has malformed options in 'sname' field: %s", err)
|
||||
}
|
||||
} else {
|
||||
s, ok := nullStr(bs[44:108])
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unterminated 'sname' string")
|
||||
}
|
||||
ret.BootServerName = s
|
||||
}
|
||||
if file {
|
||||
if err := ret.Options.Unmarshal(bs[108:236]); err != nil {
|
||||
return nil, fmt.Errorf("packet has malformed options in 'file' field: %s", err)
|
||||
}
|
||||
} else {
|
||||
s, ok := nullStr(bs[108:236])
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unterminated 'file' string")
|
||||
}
|
||||
ret.BootServerName = s
|
||||
}
|
||||
|
||||
// DHCP packets must all have at least the "DHCP Message Type"
|
||||
// option.
|
||||
typ, ok := ret.Options.Byte(53)
|
||||
if !ok {
|
||||
return nil, errors.New("packet has no DHCP Message Type")
|
||||
}
|
||||
ret.Type = MessageType(typ)
|
||||
switch ret.Type {
|
||||
case MsgDiscover, MsgRequest, MsgDecline, MsgRelease, MsgInform:
|
||||
if bs[0] != 1 {
|
||||
return nil, fmt.Errorf("BOOTP message type (%d) doesn't match DHCP message type (%s)", bs[0], ret.Type)
|
||||
}
|
||||
case MsgOffer, MsgAck, MsgNack:
|
||||
if bs[0] != 2 {
|
||||
return nil, fmt.Errorf("BOOTP message type (%d) doesn't match DHCP message type (%s", bs[0], ret.Type)
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func nullStr(bs []byte) (string, bool) {
|
||||
i := bytes.IndexByte(bs, 0)
|
||||
if i == -1 {
|
||||
return "", false
|
||||
}
|
||||
return string(bs[:i]), true
|
||||
}
|
123
dhcp/options.go
Normal file
123
dhcp/options.go
Normal file
@ -0,0 +1,123 @@
|
||||
package dhcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Options stores DHCP options.
|
||||
type Options map[int][]byte
|
||||
|
||||
// Unmarshal parses DHCP options into o.
|
||||
func (o Options) Unmarshal(bs []byte) error {
|
||||
for len(bs) > 0 {
|
||||
opt := int(bs[0])
|
||||
switch opt {
|
||||
case 0:
|
||||
// Padding byte
|
||||
bs = bs[1:]
|
||||
case 255:
|
||||
// End of options
|
||||
return nil
|
||||
default:
|
||||
// In theory, DHCP permits multiple instances of the same
|
||||
// option in a packet, as a way to have option values >255
|
||||
// bytes. Unfortunately, this is very loosely specified as
|
||||
// "up to individual options", and AFAICT, isn't at all
|
||||
// used in the wild.
|
||||
//
|
||||
// So, for now, seeing the same option twice in a packet
|
||||
// is going to be an error, until I get a bug report about
|
||||
// something that actually does it.
|
||||
if _, ok := o[opt]; ok {
|
||||
return fmt.Errorf("packet has duplicate option %d (please file a bug with a pcap!)", opt)
|
||||
}
|
||||
if len(bs) < 2 {
|
||||
return fmt.Errorf("option %d has no length byte", opt)
|
||||
}
|
||||
l := int(bs[1])
|
||||
if len(bs[2:]) < l {
|
||||
return fmt.Errorf("option %d claims to have %d bytes of payload, but only has %d bytes", opt, l, len(bs[2:]))
|
||||
}
|
||||
o[opt] = bs[2 : 2+l]
|
||||
bs = bs[2+l:]
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("options are not terminated by a 255 byte")
|
||||
}
|
||||
|
||||
// Marshal returns the wire encoding of o.
|
||||
func (o Options) Marshal() ([]byte, error) {
|
||||
var ret bytes.Buffer
|
||||
if err := o.MarshalTo(&ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret.Bytes(), nil
|
||||
}
|
||||
|
||||
// MarshalTo serializes o into w.
|
||||
func (o Options) MarshalTo(w io.Writer) error {
|
||||
opts, err := o.marshalLimited(w, 0, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(opts) > 0 {
|
||||
return errors.New("some options not written, but no limit was given (please file a bug)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// marshalLimited serializes o into w. If nBytes > 0, as many options
|
||||
// as possible are packed into that many bytes, inserting padding as
|
||||
// needed, and the remaining unwritten options are returned.
|
||||
func (o Options) marshalLimited(w io.Writer, nBytes int, skip52 bool) (Options, error) {
|
||||
ks := make([]int, 0, len(o))
|
||||
for n := range o {
|
||||
if n <= 0 || n >= 255 {
|
||||
return nil, fmt.Errorf("invalid DHCP option number %d", n)
|
||||
}
|
||||
ks = append(ks, n)
|
||||
}
|
||||
sort.Ints(ks)
|
||||
|
||||
ret := make(Options)
|
||||
for _, n := range ks {
|
||||
opt := o[n]
|
||||
if len(opt) > 255 {
|
||||
return nil, fmt.Errorf("DHCP option %d has value >255 bytes", n)
|
||||
}
|
||||
|
||||
// If space is limited, verify that we can fit the option plus
|
||||
// the final end-of-options marker.
|
||||
if nBytes > 0 && ((skip52 && n == 52) || len(opt)+3 > nBytes) {
|
||||
ret[n] = opt
|
||||
continue
|
||||
}
|
||||
|
||||
w.Write([]byte{byte(n), byte(len(opt))})
|
||||
w.Write(opt)
|
||||
nBytes -= len(opt) + 2
|
||||
}
|
||||
|
||||
w.Write([]byte{255})
|
||||
nBytes--
|
||||
if nBytes > 0 {
|
||||
w.Write(make([]byte, nBytes))
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Byte returns the value of single-byte option n, if the option value
|
||||
// is indeed a single byte.
|
||||
func (o Options) Byte(n int) (byte, bool) {
|
||||
v := o[n]
|
||||
if v == nil || len(v) != 1 {
|
||||
return 0, false
|
||||
}
|
||||
return v[0], true
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user