summaryrefslogtreecommitdiffhomepage
path: root/dhcpv4/options.go
blob: 4c70743a73e976c5e977f9e9d5478781f5c99f46 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
package dhcpv4

import (
	"errors"
	"fmt"
	"io"
	"math"
	"sort"
	"strings"

	"github.com/insomniacslk/dhcp/iana"
	"github.com/insomniacslk/dhcp/rfc1035label"
	"github.com/u-root/u-root/pkg/uio"
)

var (
	// ErrShortByteStream is an error that is thrown any time a short byte stream is
	// detected during option parsing.
	ErrShortByteStream = errors.New("short byte stream")

	// ErrZeroLengthByteStream is an error that is thrown any time a zero-length
	// byte stream is encountered.
	ErrZeroLengthByteStream = errors.New("zero-length byte stream")

	// ErrInvalidOptions is returned when invalid options data is
	// encountered during parsing. The data could report an incorrect
	// length or have trailing bytes which are not part of the option.
	ErrInvalidOptions = errors.New("invalid options data")
)

// OptionValue is an interface that all DHCP v4 options adhere to.
type OptionValue interface {
	ToBytes() []byte
	String() string
}

// Option is a DHCPv4 option and consists of a 1-byte option code and a value
// stream of bytes.
//
// The value is to be interpreted based on the option code.
type Option struct {
	Code  OptionCode
	Value OptionValue
}

// String returns a human-readable version of this option.
func (o Option) String() string {
	v := o.Value.String()
	if strings.Contains(v, "\n") {
		return fmt.Sprintf("%s:\n%s", o.Code, v)
	}
	return fmt.Sprintf("%s: %s", o.Code, v)
}

// Options is a collection of options.
type Options map[uint8][]byte

// OptionsFromList adds all given options to an options map.
func OptionsFromList(o ...Option) Options {
	opts := make(Options)
	for _, opt := range o {
		opts.Update(opt)
	}
	return opts
}

// Get will attempt to get all options that match a DHCPv4 option
// from its OptionCode.  If the option was not found it will return an
// empty list.
//
// According to RFC 3396, options that are specified more than once are
// concatenated, and hence this should always just return one option. This
// currently returns a list to be API compatible.
func (o Options) Get(code OptionCode) []byte {
	return o[code.Code()]
}

// Has checks whether o has the given opcode.
func (o Options) Has(opcode OptionCode) bool {
	_, ok := o[opcode.Code()]
	return ok
}

// Update updates the existing options with the passed option, adding it
// at the end if not present already
func (o Options) Update(option Option) {
	o[option.Code.Code()] = option.Value.ToBytes()
}

// ToBytes makes Options usable as an OptionValue as well.
//
// Used in the case of vendor-specific and relay agent options.
func (o Options) ToBytes() []byte {
	return uio.ToBigEndian(o)
}

// FromBytes 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 (o Options) FromBytes(data []byte) error {
	return o.fromBytesCheckEnd(data, false)
}

const (
	optPad = 0
	optEnd = 255
)

// FromBytesCheckEnd parses Options from byte sequences using the
// parsing function that is passed in as a paremeter
func (o Options) fromBytesCheckEnd(data []byte, checkEndOption bool) error {
	if len(data) == 0 {
		return nil
	}
	buf := uio.NewBigEndianBuffer(data)

	var end bool
	for buf.Len() >= 1 {
		// 1 byte: option code
		// 1 byte: option length n
		// n bytes: data
		code := buf.Read8()

		if code == optPad {
			continue
		} else if code == optEnd {
			end = true
			break
		}
		length := int(buf.Read8())

		// N bytes: option data
		data := buf.Consume(length)
		if data == nil {
			return fmt.Errorf("error collecting options: %v", buf.Error())
		}
		data = data[:length:length]

		// RFC 2131, Section 4.1 "Options may appear only once, [...].
		// The client concatenates the values of multiple instances of
		// the same option into a single parameter list for
		// configuration."
		//
		// See also RFC 3396 for concatenation order and options longer
		// than 255 bytes.
		o[code] = append(o[code], data...)
	}

	// If we never read the End option, the sender of this packet screwed
	// up.
	if !end && checkEndOption {
		return io.ErrUnexpectedEOF
	}

	// Any bytes left must be padding.
	for buf.Len() >= 1 {
		if buf.Read8() != optPad {
			return ErrInvalidOptions
		}
	}
	return nil
}

// sortedKeys returns an ordered slice of option keys from the Options map, for
// use in serializing options to binary.
func (o Options) sortedKeys() []int {
	// Send all values for a given key
	var codes []int
	for k := range o {
		codes = append(codes, int(k))
	}

	sort.Sort(sort.IntSlice(codes))
	return codes
}

// Marshal writes options binary representations to b.
func (o Options) Marshal(b *uio.Lexer) {
	for _, c := range o.sortedKeys() {
		code := uint8(c)
		// Even if the End option is in there, don't marshal it until
		// the end.
		if code == optEnd {
			continue
		}

		data := o[code]

		// RFC 3396: If more than 256 bytes of data are given, the
		// option is simply listed multiple times.
		for len(data) > 0 {
			// 1 byte: option code
			b.Write8(uint8(code))

			n := len(data)
			if n > math.MaxUint8 {
				n = math.MaxUint8
			}

			// 1 byte: option length
			b.Write8(uint8(n))

			// N bytes: option data
			b.WriteBytes(data[:n])
			data = data[n:]
		}
	}
}

// String prints options using DHCP-specified option codes.
func (o Options) String() string {
	return o.ToString(dhcpHumanizer)
}

// Summary prints options in human-readable values.
//
// Summary uses vendorParser to interpret the OptionVendorSpecificInformation option.
func (o Options) Summary(vendorDecoder OptionDecoder) string {
	return o.ToString(OptionHumanizer{
		ValueHumanizer: parserFor(vendorDecoder),
		CodeHumanizer: func(c uint8) OptionCode {
			return optionCode(c)
		},
	})
}

// OptionParser gives a human-legible interpretation of data for the given option code.
type OptionParser func(code OptionCode, data []byte) fmt.Stringer

// OptionHumanizer is used to interpret a set of Options for their option code
// name and values.
//
// There should be separate OptionHumanizers for each Option "space": DHCP,
// BSDP, Relay Agent Info, and others.
type OptionHumanizer struct {
	ValueHumanizer OptionParser
	CodeHumanizer  func(code uint8) OptionCode
}

// Stringify returns a human-readable interpretation of the option code and its
// associated data.
func (oh OptionHumanizer) Stringify(code uint8, data []byte) string {
	c := oh.CodeHumanizer(code)
	val := oh.ValueHumanizer(c, data)
	return fmt.Sprintf("%s: %s", c, val)
}

// dhcpHumanizer humanizes the set of DHCP option codes.
var dhcpHumanizer = OptionHumanizer{
	ValueHumanizer: parseOption,
	CodeHumanizer: func(c uint8) OptionCode {
		return optionCode(c)
	},
}

// ToString uses parse to parse options into human-readable values.
func (o Options) ToString(humanizer OptionHumanizer) string {
	var ret string
	for _, c := range o.sortedKeys() {
		code := uint8(c)
		v := o[code]
		optString := humanizer.Stringify(code, v)
		// If this option has sub structures, offset them accordingly.
		if strings.Contains(optString, "\n") {
			optString = strings.Replace(optString, "\n  ", "\n      ", -1)
		}
		ret += fmt.Sprintf("    %v\n", optString)
	}
	return ret
}

func parseOption(code OptionCode, data []byte) fmt.Stringer {
	return parserFor(nil)(code, data)
}

func parserFor(vendorParser OptionDecoder) OptionParser {
	return func(code OptionCode, data []byte) fmt.Stringer {
		return getOption(code, data, vendorParser)
	}
}

// OptionDecoder can decode a byte stream into a human-readable option.
type OptionDecoder interface {
	fmt.Stringer
	FromBytes([]byte) error
}

func getOption(code OptionCode, data []byte, vendorDecoder OptionDecoder) fmt.Stringer {
	var d OptionDecoder
	switch code {
	case OptionRouter, OptionDomainNameServer, OptionNTPServers, OptionServerIdentifier:
		d = &IPs{}

	case OptionBroadcastAddress, OptionRequestedIPAddress:
		d = &IP{}

	case OptionClientSystemArchitectureType:
		d = &iana.Archs{}

	case OptionSubnetMask:
		d = &IPMask{}

	case OptionDHCPMessageType:
		var mt MessageType
		d = &mt

	case OptionParameterRequestList:
		d = &OptionCodeList{}

	case OptionHostName, OptionDomainName, OptionRootPath,
		OptionClassIdentifier, OptionTFTPServerName, OptionBootfileName:
		var s String
		d = &s

	case OptionRelayAgentInformation:
		d = &RelayOptions{}

	case OptionDNSDomainSearchList:
		d = &rfc1035label.Labels{}

	case OptionIPAddressLeaseTime:
		var dur Duration
		d = &dur

	case OptionMaximumDHCPMessageSize:
		var u Uint16
		d = &u

	case OptionUserClassInformation:
		d = &UserClass{}

	case OptionVendorIdentifyingVendorClass:
		d = &VIVCIdentifiers{}

	case OptionVendorSpecificInformation:
		d = vendorDecoder
	}
	if d != nil && d.FromBytes(data) == nil {
		return d
	}
	return OptionGeneric{data}
}