summaryrefslogtreecommitdiffstats
path: root/src/net/udp/dhcp.c
blob: 7e9af09f7acdc037767f501e0fda3fe803fce901 (plain) (blame)
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
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
/*
 * Copyright (C) 2006 Michael Brown <mbrown@fensystems.co.uk>.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <string.h>
#include <errno.h>
#include <assert.h>
#include <byteswap.h>
#include <gpxe/netdevice.h>
#include <gpxe/dhcp.h>

/** @file
 *
 * Dynamic Host Configuration Protocol
 *
 */

/** DHCP operation types
 *
 * This table maps from DHCP message types (i.e. values of the @c
 * DHCP_MESSAGE_TYPE option) to values of the "op" field within a DHCP
 * packet.
 */
static const uint8_t dhcp_op[] = {
	[DHCPDISCOVER]	= BOOTP_REQUEST,
	[DHCPOFFER]	= BOOTP_REPLY,
	[DHCPREQUEST]	= BOOTP_REQUEST,
	[DHCPDECLINE]	= BOOTP_REQUEST,
	[DHCPACK]	= BOOTP_REPLY,
	[DHCPNAK]	= BOOTP_REPLY,
	[DHCPRELEASE]	= BOOTP_REQUEST,
	[DHCPINFORM]	= BOOTP_REQUEST,
};

/** DHCP packet option block fill order
 *
 * This is the order in which option blocks are filled when
 * reassembling a DHCP packet.  We fill the smallest field ("sname")
 * first, to maximise the chances of being able to fit large options
 * within fields which are large enough to contain them.
 */
enum dhcp_packet_option_block_fill_order {
	OPTS_SNAME = 0,
	OPTS_FILE,
	OPTS_MAIN,
	NUM_OPT_BLOCKS
};

/** DHCP option blocks within a DHCP packet
 *
 * A DHCP packet contains three fields which can be used to contain
 * options: the actual "options" field plus the "file" and "sname"
 * fields (which can be overloaded to contain options).
 */
struct dhcp_packet_option_blocks {
	struct dhcp_option_block options[NUM_OPT_BLOCKS];
};

/**
 * Set option within DHCP packet
 *
 * @v optblocks		DHCP packet option blocks
 * @v tag		DHCP option tag
 * @v data		New value for DHCP option
 * @v len		Length of value, in bytes
 * @ret option		DHCP option, or NULL
 *
 * Sets the option within the first available options block within the
 * DHCP packet.  Option blocks are tried in the order specified by @c
 * dhcp_option_block_fill_order.
 */
static struct dhcp_option *
set_dhcp_packet_option ( struct dhcp_packet_option_blocks *optblocks,
			 unsigned int tag, const void *data, size_t len ) {
	struct dhcp_option_block *options;
	struct dhcp_option *option;

	for ( options = optblocks->options ;
	      options < &optblocks->options[NUM_OPT_BLOCKS] ; options++ ) {
		option = set_dhcp_option ( options, tag, data, len );
		if ( option )
			return option;
	}
	return NULL;
}

/**
 * Copy options to DHCP packet
 *
 * @v optblocks		DHCP packet option blocks
 * @v encapsulator	Encapsulating option, or zero
 * @ret rc		Return status code
 * 
 * Copies options from DHCP options blocks into a DHCP packet.  Most
 * options are copied verbatim.  Recognised encapsulated options
 * fields are handled as such.  Selected options (e.g. @c
 * DHCP_OPTION_OVERLOAD) are always ignored, since these special cases
 * are handled by other code.
 */
static int
copy_dhcp_options_to_packet ( struct dhcp_packet_option_blocks *optblocks,
			      unsigned int encapsulator ) {
	unsigned int subtag;
	unsigned int tag;
	struct dhcp_option *option;
	struct dhcp_option *copied;
	int rc;

	for ( subtag = DHCP_MIN_OPTION; subtag <= DHCP_MAX_OPTION; subtag++ ) {
		tag = DHCP_ENCAP_OPT ( encapsulator, subtag );
		switch ( tag ) {
		case DHCP_OPTION_OVERLOAD:
			/* Hard-coded in packets we reassemble; skip
			 * this option
			 */
			break;
		case DHCP_EB_ENCAP:
		case DHCP_VENDOR_ENCAP:
			/* Process encapsulated options field */
			if ( ( rc = copy_dhcp_options_to_packet ( optblocks,
								  tag ) ) != 0)
				return rc;
			break;
		default:
			/* Copy option to reassembled packet */
			option = find_global_dhcp_option ( tag );
			if ( ! option )
				break;
			copied = set_dhcp_packet_option ( optblocks, tag,
							  &option->data,
							  option->len );
			if ( ! copied )
				return -ENOSPC;
			break;
		};
	}

	return 0;
}

/**
 * Assemble a DHCP packet
 *
 * @v dhcp		DHCP session
 * @v data		Packet to be filled in
 * @v max_len		Length of packet buffer
 * @ret len		Length of assembled packet
 *
 * Reconstruct a DHCP packet from a DHCP options list.
 */
size_t dhcp_assemble ( struct dhcp_session *dhcp, void *data,
		       size_t max_len ) {
	struct dhcp_packet *dhcppkt = data;
	struct dhcp_option *option;
	struct dhcp_packet_option_blocks optblocks;
	unsigned int dhcp_message_type;
	static const uint8_t overloading = ( DHCP_OPTION_OVERLOAD_FILE |
					     DHCP_OPTION_OVERLOAD_SNAME );

	/* Fill in constant fields */
	memset ( dhcppkt, 0, max_len );
	dhcppkt->xid = dhcp->xid;
	dhcppkt->magic = htonl ( DHCP_MAGIC_COOKIE );

	/* Derive "op" field from DHCP_MESSAGE_TYPE option value */
	dhcp_message_type = find_global_dhcp_num_option ( DHCP_MESSAGE_TYPE );
	dhcppkt->op = dhcp_op[dhcp_message_type];

	/* Fill in NIC details */
	dhcppkt->htype = ntohs ( dhcp->netdev->ll_protocol->ll_proto );
	dhcppkt->hlen = dhcp->netdev->ll_protocol->ll_addr_len;
	memcpy ( dhcppkt->chaddr, dhcp->netdev->ll_addr, dhcppkt->hlen );

	/* Fill in IP addresses if present */
	option = find_global_dhcp_option ( DHCP_EB_YIADDR );
	if ( option ) {
		memcpy ( &dhcppkt->yiaddr, &option->data,
			 sizeof ( dhcppkt->yiaddr ) );
	}
	option = find_global_dhcp_option ( DHCP_EB_SIADDR );
	if ( option ) {
		memcpy ( &dhcppkt->siaddr, &option->data,
			 sizeof ( dhcppkt->siaddr ) );
	}

	/* Initialise option blocks */
	init_dhcp_options ( &optblocks.options[OPTS_MAIN], dhcppkt->options,
			    ( max_len -
			      offsetof ( typeof ( *dhcppkt ), options ) ) );
	init_dhcp_options ( &optblocks.options[OPTS_FILE], dhcppkt->file,
			    sizeof ( dhcppkt->file ) );
	init_dhcp_options ( &optblocks.options[OPTS_SNAME], dhcppkt->sname,
			    sizeof ( dhcppkt->sname ) );
	set_dhcp_option ( &optblocks.options[OPTS_MAIN], DHCP_OPTION_OVERLOAD,
			  &overloading, sizeof ( overloading ) );

	/* Populate option blocks */
	copy_dhcp_options_to_packet ( &optblocks, 0 );

	return ( offsetof ( typeof ( *dhcppkt ), options )
		 + optblocks.options[OPTS_MAIN].len );
}

/**
 * Calculate used length of a field containing DHCP options
 *
 * @v data		Field containing DHCP options
 * @v max_len		Field length
 * @ret len		Used length (excluding the @c DHCP_END tag)
 */
static size_t dhcp_field_len ( const void *data, size_t max_len ) {
	struct dhcp_option_block options;
	struct dhcp_option *end;

	options.data = ( ( void * ) data );
	options.len = max_len;
	end = find_dhcp_option ( &options, DHCP_END );
	return ( end ? ( ( ( void * ) end ) - data ) : 0 );
}

/**
 * Merge field containing DHCP options or string into DHCP options block
 *
 * @v options		DHCP option block
 * @v data		Field containing DHCP options
 * @v max_len		Field length
 * @v tag		DHCP option tag, or 0
 *
 * If @c tag is non-zero, the field will be treated as a
 * NUL-terminated string representing the value of the specified DHCP
 * option.  If @c tag is zero, the field will be treated as a block of
 * DHCP options, and simply appended to the existing options in the
 * option block.
 *
 * The caller must ensure that there is enough space in the options
 * block to perform the merge.
 */
static void merge_dhcp_field ( struct dhcp_option_block *options,
			       const void *data, size_t max_len,
			       unsigned int tag ) {
	size_t len;
	void *dest;
	struct dhcp_option *end;

	if ( tag ) {
		set_dhcp_option ( options, tag, data, strlen ( data ) );
	} else {
		len = dhcp_field_len ( data, max_len );
		dest = ( options->data + options->len - 1 );
		memcpy ( dest, data, len );
		options->len += len;
		end = ( dest + len );
		end->tag = DHCP_END;
	}
}

/**
 * Parse DHCP packet and construct DHCP options block
 *
 * @v data		DHCP packet
 * @v len		Length of DHCP packet
 * @ret options		DHCP options block, or NULL
 *
 * Parses a received DHCP packet and canonicalises its contents into a
 * single DHCP options block.  The "file" and "sname" fields are
 * converted into the corresponding DHCP options (@c
 * DHCP_BOOTFILE_NAME and @c DHCP_TFTP_SERVER_NAME respectively).  If
 * these fields are used for option overloading, their options are
 * merged in to the options block.  The values of the "yiaddr" and
 * "siaddr" fields will be stored within the options block as the
 * options @c DHCP_EB_YIADDR and @c DHCP_EB_SIADDR.
 * 
 * Note that this call allocates new memory for the constructed DHCP
 * options block; it is the responsibility of the caller to eventually
 * free this memory.
 */
struct dhcp_option_block * dhcp_parse ( const void *data, size_t len ) {
	const struct dhcp_packet *dhcppkt = data;
	struct dhcp_option_block *options;
	size_t options_len;
	unsigned int overloading;

	/* Sanity check */
	if ( len < sizeof ( *dhcppkt ) )
		return NULL;

	/* Calculate size of resulting concatenated option block:
	 *
	 *   The "options" field : length of the field minus the DHCP_END tag.
	 *
	 *   The "file" field : maximum length of the field minus the
	 *   NUL terminator, plus a 2-byte DHCP header or, if used for
	 *   option overloading, the length of the field minus the
	 *   DHCP_END tag.
	 *
	 *   The "sname" field : as for the "file" field.
	 *
	 *   15 bytes for an encapsulated options field to contain the
	 *   value of the "yiaddr" and "siaddr" fields
	 *
	 *   1 byte for a final terminating DHCP_END tag.
	 */
	options_len = ( ( len - offsetof ( typeof ( *dhcppkt ), options ) ) - 1
			+ ( sizeof ( dhcppkt->file ) + 1 )
			+ ( sizeof ( dhcppkt->sname ) + 1 )
			+ 15 /* yiaddr and siaddr */
			+ 1 /* DHCP_END tag */ );
	
	/* Allocate empty options block of required size */
	options = alloc_dhcp_options ( options_len );
	if ( ! options ) {
		DBG ( "DHCP could not allocate %d-byte option block\n",
		      options_len );
		return NULL;
	}
	
	/* Merge in "options" field, if this is a DHCP packet */
	if ( dhcppkt->magic == htonl ( DHCP_MAGIC_COOKIE ) ) {
		merge_dhcp_field ( options, dhcppkt->options,
				   ( len -
				     offsetof ( typeof (*dhcppkt), options ) ),
				   0 /* Always contains options */ );
	}

	/* Identify overloaded fields */
	overloading = find_dhcp_num_option ( options, DHCP_OPTION_OVERLOAD );
	
	/* Merge in "file" and "sname" fields */
	merge_dhcp_field ( options, dhcppkt->file, sizeof ( dhcppkt->file ),
			   ( ( overloading & DHCP_OPTION_OVERLOAD_FILE ) ?
			     DHCP_BOOTFILE_NAME : 0 ) );
	merge_dhcp_field ( options, dhcppkt->sname, sizeof ( dhcppkt->sname ),
			   ( ( overloading & DHCP_OPTION_OVERLOAD_SNAME ) ?
			     DHCP_TFTP_SERVER_NAME : 0 ) );

	/* Set options for "yiaddr" and "siaddr", if present */
	if ( dhcppkt->yiaddr.s_addr ) {
		set_dhcp_option ( options, DHCP_EB_YIADDR,
				  &dhcppkt->yiaddr, sizeof (dhcppkt->yiaddr) );
	}
	if ( dhcppkt->siaddr.s_addr ) {
		set_dhcp_option ( options, DHCP_EB_SIADDR,
				  &dhcppkt->siaddr, sizeof (dhcppkt->siaddr) );
	}
	
	assert ( options->len <= options->max_len );

	return options;
}