diff --git a/dhcp/dhcp.go b/dhcp/dhcp.go new file mode 100644 index 0000000..f1d17d1 --- /dev/null +++ b/dhcp/dhcp.go @@ -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("", 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 +} diff --git a/dhcp/options.go b/dhcp/options.go new file mode 100644 index 0000000..1ceb300 --- /dev/null +++ b/dhcp/options.go @@ -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 +}