1 /*
2  * The dhcpd-pools has BSD 2-clause license which also known as "Simplified
3  * BSD License" or "FreeBSD License".
4  *
5  * Copyright 2006- Sami Kerola. All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions are
9  * met:
10  *
11  *    1. Redistributions of source code must retain the above copyright
12  *       notice, this list of conditions and the following disclaimer.
13  *
14  *    2. Redistributions in binary form must reproduce the above copyright
15  *       notice, this list of conditions and the following disclaimer in the
16  *       documentation and/or other materials provided with the
17  *       distribution.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
20  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
22  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR AND CONTRIBUTORS OR
23  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
24  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
25  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
26  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
27  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
28  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  *
31  * The views and conclusions contained in the software and documentation are
32  * those of the authors and should not be interpreted as representing
33  * official policies, either expressed or implied, of Sami Kerola.
34  */
35 
36 /*! \file getdata.c
37  * \brief Functions to read data from dhcpd.conf and dhcdp.leases files.
38  */
39 
40 #include <config.h>
41 
42 #include <arpa/inet.h>
43 #include <ctype.h>
44 #include <errno.h>
45 #include <fcntl.h>
46 #include <limits.h>
47 #include <netinet/in.h>
48 #include <stddef.h>
49 #include <stdint.h>
50 #include <stdio.h>
51 #include <stdlib.h>
52 #include <sys/stat.h>
53 
54 #include "error.h"
55 #include "xalloc.h"
56 
57 #include "dhcpd-pools.h"
58 
59 /*! \enum dhcpd_magic_numbers
60  * \brief MAXLEN is maximum expected line length in dhcpd.conf and
61  * dhcpd.leases.
62  */
63 enum dhcpd_magic_numbers {
64 	MAXLEN = 1024
65 };
66 
67 /*! \enum isc_conf_parser
68  * \brief Configuration file parsing state flags.  The
69  * is_interesting_config_clause() will return one of these to parse_config().
70  */
71 enum isc_conf_parser {
72 	ITS_NOTHING_INTERESTING,
73 	ITS_A_RANGE_FIRST_IP,
74 	ITS_A_RANGE_SECOND_IP,
75 	ITS_A_SHAREDNET,
76 	ITS_AN_INCLUDE,
77 	ITS_A_SUBNET,
78 	ITS_A_NETMASK
79 };
80 
81 /*! \brief Lease file parser.  The parser can only read ISC DHCPD
82  * dhcpd.leases file format.  */
parse_leases(struct conf_t * state)83 int parse_leases(struct conf_t *state)
84 {
85 	FILE *dhcpd_leases;
86 	char *line, *ipstring, macstring[20], *stop;
87 	union ipaddr_t addr;
88 	struct leases_t *lease;
89 
90 	dhcpd_leases = fopen(state->dhcpdlease_file, "r");
91 	if (dhcpd_leases == NULL)
92 		error(EXIT_FAILURE, errno, "parse_leases: %s", state->dhcpdlease_file);
93 #ifdef HAVE_POSIX_FADVISE
94 # ifdef POSIX_FADV_SEQUENTIAL
95 	if (posix_fadvise(fileno(dhcpd_leases), 0, 0, POSIX_FADV_SEQUENTIAL) != 0)
96 		error(EXIT_FAILURE, errno, "parse_leases: fadvise %s", state->dhcpdlease_file);
97 # endif				/* POSIX_FADV_SEQUENTIAL */
98 #endif				/* HAVE_POSIX_FADVISE */
99 	line = xmalloc(sizeof(char) * MAXLEN);
100 	line[0] = '\0';
101 	ipstring = xmalloc(sizeof(char) * MAXLEN);
102 	ipstring[0] = '\0';
103 	while (!feof(dhcpd_leases)) {
104 		if (!fgets(line, MAXLEN, dhcpd_leases) && ferror(dhcpd_leases))
105 			error(EXIT_FAILURE, errno, "parse_leases: %s", state->dhcpdlease_file);
106 		switch (xstrstr(state, line)) {
107 			/* It's a lease, save IP */
108 		case PREFIX_LEASE:
109 			stop =
110 			    memccpy(ipstring,
111 				    line + (state->ip_version ==
112 					    IPv4 ? 6 : 9), ' ', strlen(line));
113 			if (stop != NULL) {
114 				--stop;
115 				*stop = '\0';
116 			}
117 			parse_ipaddr(state, ipstring, &addr);
118 			break;
119 		case PREFIX_BINDING_STATE_FREE:
120 		case PREFIX_BINDING_STATE_ABANDONED:
121 		case PREFIX_BINDING_STATE_EXPIRED:
122 		case PREFIX_BINDING_STATE_RELEASED:
123 			if ((lease = find_lease(state, &addr)) != NULL)
124 				delete_lease(state, lease);
125 			add_lease(state, &addr, FREE);
126 			break;
127 		case PREFIX_BINDING_STATE_ACTIVE:
128 			/* remove old entry, if exists */
129 			if ((lease = find_lease(state, &addr)) != NULL)
130 				delete_lease(state, lease);
131 			add_lease(state, &addr, ACTIVE);
132 			break;
133 		case PREFIX_BINDING_STATE_BACKUP:
134 			/* remove old entry, if exists */
135 			if ((lease = find_lease(state, &addr)) != NULL)
136 				delete_lease(state, lease);
137 			add_lease(state, &addr, BACKUP);
138 			state->backups_found = 1;
139 			break;
140 		case PREFIX_HARDWARE_ETHERNET:
141 			if (state->print_mac_addreses == 0)
142 				break;
143 			memcpy(macstring, line + 20, 17);
144 			macstring[17] = '\0';
145 			if ((lease = find_lease(state, &addr)) != NULL)
146 				lease->ethernet = xstrdup(macstring);
147 			break;
148 		default:
149 			/* do nothing */ ;
150 		}
151 	}
152 #undef HAS_PREFIX
153 	free(line);
154 	free(ipstring);
155 	fclose(dhcpd_leases);
156 	return 0;
157 }
158 
159 /*! \brief Keyword search in dhcpd.conf file.
160  * \param s A line from the dhcpd.conf file.
161  * \return Indicator what configuration was found. */
is_interesting_config_clause(struct conf_t * state,char const * restrict s)162 static int is_interesting_config_clause(struct conf_t *state, char const *restrict s)
163 {
164 	if (strstr(s, "range"))
165 		return ITS_A_RANGE_FIRST_IP;
166 	if (strstr(s, "shared-network"))
167 		return ITS_A_SHAREDNET;
168 	if (state->all_as_shared) {
169 		if (strstr(s, "subnet"))
170 			return ITS_A_SUBNET;
171 		if (strstr(s, "netmask"))
172 			return ITS_A_NETMASK;
173 	}
174 	if (strstr(s, "include"))
175 		return ITS_AN_INCLUDE;
176 	return ITS_NOTHING_INTERESTING;
177 }
178 
179 /*! \brief Flip first and last IP in range if they are in unusual order.
180  */
reorder_last_first(struct range_t * range_p)181 static void reorder_last_first(struct range_t *range_p)
182 {
183 	if (ipcomp(&range_p->first_ip, &range_p->last_ip) > 0) {
184 		union ipaddr_t tmp;
185 
186 		tmp = range_p->first_ip;
187 		range_p->first_ip = range_p->last_ip;
188 		range_p->last_ip = tmp;
189 	}
190 }
191 
192 /*! \brief The dhcpd.conf file parser.
193  * FIXME: This spaghetti monster function needs to be rewrote at least
194  * ones more.
195  */
parse_config(struct conf_t * state,const int is_include,const char * restrict config_file,struct shared_network_t * restrict shared_p)196 void parse_config(struct conf_t *state, const int is_include, const char *restrict config_file,
197 		  struct shared_network_t *restrict shared_p)
198 {
199 	FILE *dhcpd_config;
200 	int newclause = 1, comment = 0, one_ip_range = 0; /* booleans */
201 	int quote = 0, braces = 0, argument = ITS_NOTHING_INTERESTING;
202 	size_t i = 0;
203 	char *word;
204 	int braces_shared = 1000;
205 	union ipaddr_t addr;
206 	struct range_t *range_p = NULL;
207 
208 	word = xmalloc(sizeof(char) * MAXLEN);
209 	if (is_include)
210 		/* Default place holder for ranges "All networks". */
211 		shared_p->name = state->shared_net_root->name;
212 	/* Open configuration file */
213 	dhcpd_config = fopen(config_file, "r");
214 	if (dhcpd_config == NULL)
215 		error(EXIT_FAILURE, errno, "parse_config: %s", config_file);
216 #ifdef HAVE_POSIX_FADVISE
217 # ifdef POSIX_FADV_SEQUENTIAL
218 	if (posix_fadvise(fileno(dhcpd_config), 0, 0, POSIX_FADV_SEQUENTIAL) != 0)
219 		error(EXIT_FAILURE, errno, "parse_config: fadvise %s", config_file);
220 # endif				/* POSIX_FADV_SEQUENTIAL */
221 #endif				/* HAVE_POSIX_FADVISE */
222 	/* Very hairy stuff begins. */
223 	while (unlikely(!feof(dhcpd_config))) {
224 		int c;
225 
226 		c = fgetc(dhcpd_config);
227 		if (CHAR_MAX < c)
228 			continue;
229 		/* Certain characters are magical */
230 		switch (c) {
231 			/* Handle comments if they are not quoted */
232 		case '#':
233 			if (quote == 0)
234 				comment = 1;
235 			continue;
236 		case '"':
237 			if (comment == 0) {
238 				quote++;
239 				/* Either one or zero */
240 				quote = quote % 2;
241 			}
242 			continue;
243 		case '\n':
244 			/* New line resets comment section, but
245 			 * not if quoted */
246 			if (quote == 0)
247 				comment = 0;
248 			break;
249 		case ';':
250 			/* Quoted colon does not mean new clause */
251 			if (0 < quote)
252 				break;
253 			if (comment == 0
254 			    && argument != ITS_A_RANGE_FIRST_IP
255 			    && argument != ITS_A_RANGE_SECOND_IP && argument != ITS_AN_INCLUDE) {
256 				newclause = 1;
257 				i = 0;
258 			} else if (argument == ITS_A_RANGE_FIRST_IP && one_ip_range == 1) {
259 				argument = ITS_A_RANGE_SECOND_IP;
260 				c = ' ';
261 			} else if (argument == ITS_A_RANGE_SECOND_IP && 0 < i) {
262 				/* Range ends to ; and this hair in code
263 				 * make two ranges wrote together like...
264 				 *
265 				 * range 10.20.30.40 10.20.30.41;range 10.20.30.42 10.20.30.43;
266 				 *
267 				 * ...to be interpreted correctly. */
268 				c = ' ';
269 				break;
270 			} else if (argument == ITS_A_RANGE_SECOND_IP && i == 0) {
271 				if (!range_p) {
272 					long int pos;
273 					pos = ftell(dhcpd_config);
274 					error(EXIT_FAILURE, 0, "parse_config: parsing failed at position: %ld", pos);
275 				}
276 				range_p->last_ip = range_p->first_ip;
277 				goto newrange;
278 			}
279 			continue;
280 		case '{':
281 			if (0 < quote)
282 				break;
283 			if (comment == 0)
284 				braces++;
285 			/* i == 0 detects word that ends to brace like:
286 			 *
287 			 * shared-network DSL{ ... */
288 			if (i == 0) {
289 				newclause = 1;
290 				continue;
291 			}
292 			break;
293 		case '}':
294 			if (0 < quote)
295 				break;
296 			if (comment == 0) {
297 				braces--;
298 				/* End of shared-network */
299 				if (braces_shared == braces) {
300 					/* FIXME: Using 1000 is lame, but
301 					 * works. */
302 					braces_shared = 1000;
303 					shared_p = state->shared_net_root;
304 				}
305 				/* Not literally 1, but works for this
306 				 * program */
307 				newclause = 1;
308 			}
309 			continue;
310 		default:
311 			break;
312 		}
313 		/* Either inside comment or Nth word of clause. */
314 		if (comment == 1 || (newclause == 0 && argument == ITS_NOTHING_INTERESTING))
315 			continue;
316 		/* Strip white spaces before new clause word. */
317 		if ((newclause == 1 || argument != ITS_NOTHING_INTERESTING)
318 		    && isspace(c) && i == 0 && one_ip_range == 0)
319 			continue;
320 		/* Save to word which clause this is. */
321 		if ((newclause == 1 || argument != ITS_NOTHING_INTERESTING)
322 		    && (!isspace(c) || 0 < quote)) {
323 			word[i] = (char) c;
324 			i++;
325 			/* Long word which is almost causing overflow. None
326 			 * of words are this long which the program is
327 			 * searching. */
328 			if (MAXLEN == i) {
329 				newclause = 0;
330 				i = 0;
331 				continue;
332 			}
333 		}
334 		/* See if clause is something that parser is looking for. */
335 		else if (newclause == 1) {
336 			/* Insert string end & set state */
337 			word[i] = '\0';
338 			if (word[i - 1] != '{')
339 				newclause = 0;
340 			i = 0;
341 			argument = is_interesting_config_clause(state, word);
342 			if (argument == ITS_A_RANGE_FIRST_IP)
343 				one_ip_range = 1;
344 		}
345 		/* words after range, shared-network or include */
346 		else if (argument != ITS_NOTHING_INTERESTING) {
347 			word[i] = '\0';
348 			newclause = 0;
349 			i = 0;
350 
351 			switch (argument) {
352 			case ITS_A_RANGE_SECOND_IP:
353 				/* printf ("range 2nd ip: %s\n", word); */
354 				range_p = state->ranges + state->num_ranges;
355 				argument = ITS_NOTHING_INTERESTING;
356 				if (strchr(word, '/')) {
357 					parse_cidr(state, range_p, word);
358 					one_ip_range = 0;
359 				} else {
360 					/* not cidr */
361 					parse_ipaddr(state, word, &addr);
362 					if (one_ip_range == 1) {
363 						one_ip_range = 0;
364 						copy_ipaddr(&range_p->first_ip, &addr);
365 					}
366 					copy_ipaddr(&range_p->last_ip, &addr);
367 					reorder_last_first(range_p);
368 				}
369  newrange:
370 				range_p->count = 0;
371 				range_p->touched = 0;
372 				range_p->backups = 0;
373 				range_p->shared_net = shared_p;
374 				state->num_ranges++;
375 				if (state->ranges_size <= state->num_ranges) {
376 					state->ranges_size *= 2;
377 					state->ranges = xrealloc(state->ranges, sizeof(struct range_t) * state->ranges_size);
378 					range_p = state->ranges + state->num_ranges;
379 				}
380 				newclause = 1;
381 				break;
382 			case ITS_A_RANGE_FIRST_IP:
383 				/* printf ("range 1st ip: %s\n", word); */
384 				range_p = state->ranges + state->num_ranges;
385 				if (!(parse_ipaddr(state, word, &addr)))
386 					/* word was not ip, try again */
387 					break;
388 				copy_ipaddr(&range_p->first_ip, &addr);
389 				one_ip_range = 0;
390 				argument = ITS_A_RANGE_SECOND_IP;
391 				break;
392 			case ITS_A_SHAREDNET:
393 			case ITS_A_SUBNET:
394 				/* ignore subnets inside a shared-network */
395 				if (argument == ITS_A_SUBNET && shared_p != state->shared_net_root) {
396 					argument = ITS_NOTHING_INTERESTING;
397 					break;
398 				}
399 				state->shared_net_head->next = xcalloc(sizeof(struct shared_network_t), 1);
400 				state->shared_net_head = state->shared_net_head->next;
401 				shared_p = state->shared_net_head;
402 				shared_p->name = xstrdup(word);
403 				shared_p->netmask = (argument == ITS_A_SUBNET ? -1 : 0); /* do not fill in netmask */
404 				/* record network's mask too */
405 				if (argument == ITS_A_SUBNET)
406 				        newclause = 1;
407 				argument = ITS_NOTHING_INTERESTING;
408 				braces_shared = braces;
409 				break;
410 			case ITS_A_NETMASK:
411 				/* fill in only when requested to do so */
412 				if (shared_p->netmask) {
413 					if (!(parse_ipaddr(state, word, &addr)))
414 						break;
415 					shared_p->netmask = 32;
416 					while ((addr.v4 & 0x01) == 0) {
417 						addr.v4 >>= 1;
418 						shared_p->netmask--;
419 					}
420 					snprintf(word, MAXLEN-1, "%s/%d", shared_p->name, shared_p->netmask);
421 					if (shared_p->name)
422 						free(shared_p->name);
423 					shared_p->name = xstrdup(word);
424 				}
425 				argument = ITS_NOTHING_INTERESTING;
426 				braces_shared = braces;
427 				break;
428 			case ITS_AN_INCLUDE:
429 				/* printf ("include file: %s\n", word); */
430 				argument = ITS_NOTHING_INTERESTING;
431 				parse_config(state, 0, word, shared_p);
432 				newclause = 1;
433 				break;
434 			case ITS_NOTHING_INTERESTING:
435 				/* printf ("nothing interesting: %s\n", word); */
436 				argument = ITS_NOTHING_INTERESTING;
437 				break;
438 			default:
439 				puts("impossible occurred, report a bug");
440 				abort();
441 			}
442 		}
443 	}
444 	free(word);
445 	fclose(dhcpd_config);
446 	return;
447 }
448