hosts_access.c revision 1.23 1 /* $NetBSD: hosts_access.c,v 1.23 2021/03/07 15:09:12 christos Exp $ */
2
3 /*
4 * This module implements a simple access control language that is based on
5 * host (or domain) names, NIS (host) netgroup names, IP addresses (or
6 * network numbers) and daemon process names. When a match is found the
7 * search is terminated, and depending on whether PROCESS_OPTIONS is defined,
8 * a list of options is executed or an optional shell command is executed.
9 *
10 * Host and user names are looked up on demand, provided that suitable endpoint
11 * information is available as sockaddr_in structures or TLI netbufs. As a
12 * side effect, the pattern matching process may change the contents of
13 * request structure fields.
14 *
15 * Diagnostics are reported through syslog(3).
16 *
17 * Compile with -DNETGROUP if your library provides support for netgroups.
18 *
19 * Author: Wietse Venema, Eindhoven University of Technology, The Netherlands.
20 */
21
22 #include <sys/cdefs.h>
23 #ifndef lint
24 #if 0
25 static char sccsid[] = "@(#) hosts_access.c 1.21 97/02/12 02:13:22";
26 #else
27 __RCSID("$NetBSD: hosts_access.c,v 1.23 2021/03/07 15:09:12 christos Exp $");
28 #endif
29 #endif
30
31 /* System libraries. */
32
33 #include <sys/types.h>
34 #include <sys/param.h>
35 #ifdef INET6
36 #include <sys/socket.h>
37 #endif
38 #include <netinet/in.h>
39 #include <arpa/inet.h>
40 #include <blocklist.h>
41 #include <stdio.h>
42 #include <stdlib.h>
43 #include <syslog.h>
44 #include <ctype.h>
45 #include <errno.h>
46 #include <setjmp.h>
47 #include <string.h>
48 #include <netdb.h>
49 #ifdef NETGROUP
50 #include <netgroup.h>
51 #include <rpcsvc/ypclnt.h>
52 #endif
53
54 /* Local stuff. */
55
56 #include "tcpd.h"
57
58 /* Error handling. */
59
60 extern jmp_buf tcpd_buf;
61
62 /* Delimiters for lists of daemons or clients. */
63
64 static char sep[] = ", \t\r\n";
65
66 /* Constants to be used in assignments only, not in comparisons... */
67
68 #define YES 1
69 #define NO 0
70
71 /*
72 * These variables are globally visible so that they can be redirected in
73 * verification mode.
74 */
75
76 const char *hosts_allow_table = HOSTS_ALLOW;
77 const char *hosts_deny_table = HOSTS_DENY;
78 int hosts_access_verbose = 0;
79
80 /*
81 * In a long-running process, we are not at liberty to just go away.
82 */
83
84 int resident = (-1); /* -1, 0: unknown; +1: yes */
85
86 /* Forward declarations. */
87
88 static int table_match(const char *, struct request_info *);
89 static int list_match(char *, struct request_info *,
90 int (*)(char *, struct request_info *));
91 static int server_match(char *, struct request_info *);
92 static int client_match(char *, struct request_info *);
93 static int host_match(char *, struct host_info *);
94 static int hostfile_match(char *, struct host_info *);
95 static int rbl_match(char *, char *);
96 static int string_match(char *, char *);
97 static int masked_match(char *, char *, char *);
98 static int masked_match4(char *, char *, char *);
99 #ifdef INET6
100 static int masked_match6(char *, char *, char *);
101 #endif
102
103 /* Size of logical line buffer. */
104
105 #define BUFLEN 2048
106
107 static void
108 pfilter_notify(struct request_info *request, int b)
109 {
110 static struct blocklist *blstate;
111
112 if (blstate == NULL) {
113 blstate = blocklist_open();
114 }
115 if (request->client->sin != NULL) {
116 blocklist_sa_r(blstate, b, request->fd != -1 ? request->fd : 3,
117 request->client->sin, request->client->sin->sa_len,
118 request->daemon ? request->daemon : getprogname());
119 } else {
120 blocklist_r(blstate, b, (request->fd != -1) ? request->fd : 3,
121 request->daemon ? request->daemon : getprogname());
122 }
123 }
124
125 /* hosts_access - host access control facility */
126
127 int
128 hosts_access(struct request_info *request)
129 {
130 int verdict;
131
132 /*
133 * If the (daemon, client) pair is matched by an entry in the file
134 * /etc/hosts.allow, access is granted. Otherwise, if the (daemon,
135 * client) pair is matched by an entry in the file /etc/hosts.deny,
136 * access is denied. Otherwise, access is granted. A non-existent
137 * access-control file is treated as an empty file.
138 *
139 * After a rule has been matched, the optional language extensions may
140 * decide to grant or refuse service anyway. Or, while a rule is being
141 * processed, a serious error is found, and it seems better to play safe
142 * and deny service. All this is done by jumping back into the
143 * hosts_access() routine, bypassing the regular return from the
144 * table_match() function calls below.
145 */
146
147 if (resident <= 0)
148 resident++;
149 verdict = setjmp(tcpd_buf);
150 if (verdict != 0) {
151 if (verdict != AC_PERMIT)
152 pfilter_notify(request, BLOCKLIST_AUTH_FAIL);
153 /* XXX pfilter_notify(0)??? */
154 return (verdict == AC_PERMIT);
155 }
156 if (table_match(hosts_allow_table, request)) {
157 /* XXX pfilter_notify(0)??? */
158 return (YES);
159 }
160 if (table_match(hosts_deny_table, request)) {
161 pfilter_notify(request, BLOCKLIST_AUTH_FAIL);
162 return (NO);
163 }
164 /* XXX pfilter_notify(0)??? */
165 return (YES);
166 }
167
168 /* table_match - match table entries with (daemon, client) pair */
169
170 static int
171 table_match(const char *table, struct request_info *request)
172 {
173 FILE *fp;
174 char sv_list[BUFLEN]; /* becomes list of daemons */
175 char *cl_list; /* becomes list of clients */
176 char *sh_cmd = NULL; /* becomes optional shell command */
177 int match = NO;
178 struct tcpd_context saved_context;
179
180 saved_context = tcpd_context; /* stupid compilers */
181
182 /*
183 * Between the fopen() and fclose() calls, avoid jumps that may cause
184 * file descriptor leaks.
185 */
186
187 if ((fp = fopen(table, "r")) != 0) {
188 tcpd_context.file = table;
189 tcpd_context.line = 0;
190 while (match == NO && xgets(sv_list, sizeof(sv_list), fp) != 0) {
191 if (sv_list[strlen(sv_list) - 1] != '\n') {
192 tcpd_warn("missing newline or line too long");
193 continue;
194 }
195 if (sv_list[0] == '#' || sv_list[strspn(sv_list, " \t\r\n")] == 0)
196 continue;
197 if ((cl_list = split_at(sv_list, ':')) == 0) {
198 tcpd_warn("missing \":\" separator");
199 continue;
200 }
201 sh_cmd = split_at(cl_list, ':');
202 match = list_match(sv_list, request, server_match)
203 && list_match(cl_list, request, client_match);
204 }
205 (void) fclose(fp);
206 } else if (errno != ENOENT) {
207 tcpd_warn("cannot open %s: %m", table);
208 }
209 if (match) {
210 if (hosts_access_verbose > 1)
211 syslog(LOG_DEBUG, "matched: %s line %d",
212 tcpd_context.file, tcpd_context.line);
213 if (sh_cmd) {
214 #ifdef PROCESS_OPTIONS
215 process_options(sh_cmd, request);
216 #else
217 char cmd[BUFSIZ];
218 shell_cmd(percent_x(cmd, sizeof(cmd), sh_cmd, request));
219 #endif
220 }
221 }
222 tcpd_context = saved_context;
223 return (match);
224 }
225
226 /* list_match - match a request against a list of patterns with exceptions */
227
228 static int
229 list_match(char *list, struct request_info *request,
230 int (*match_fn)(char *, struct request_info *))
231 {
232 char *tok;
233 static char *last;
234 int l;
235
236 /*
237 * Process tokens one at a time. We have exhausted all possible matches
238 * when we reach an "EXCEPT" token or the end of the list. If we do find
239 * a match, look for an "EXCEPT" list and recurse to determine whether
240 * the match is affected by any exceptions.
241 */
242
243 for (tok = strtok_r(list, sep, &last); tok != 0;
244 tok = strtok_r(NULL, sep, &last)) {
245 if (STR_EQ(tok, "EXCEPT")) /* EXCEPT: give up */
246 return (NO);
247 l = strlen(tok);
248 if (*tok == '[' && tok[l - 1] == ']') {
249 tok[l - 1] = '\0';
250 tok++;
251 }
252 if (match_fn(tok, request)) { /* YES: look for exceptions */
253 while ((tok = strtok_r(NULL, sep, &last)) && STR_NE(tok, "EXCEPT"))
254 /* VOID */ ;
255 return (tok == 0 || list_match(NULL, request, match_fn) == 0);
256 }
257 }
258 return (NO);
259 }
260
261 /* server_match - match server information */
262
263 static int
264 server_match(char *tok, struct request_info *request)
265 {
266 char *host;
267
268 if ((host = split_at(tok + 1, '@')) == 0) { /* plain daemon */
269 return (string_match(tok, eval_daemon(request)));
270 } else { /* daemon@host */
271 return (string_match(tok, eval_daemon(request))
272 && host_match(host, request->server));
273 }
274 }
275
276 /* client_match - match client information */
277
278 static int
279 client_match(char *tok, struct request_info *request)
280 {
281 char *host;
282
283 if ((host = split_at(tok + 1, '@')) == 0) { /* plain host */
284 return (host_match(tok, request->client));
285 } else { /* user@host */
286 return (host_match(host, request->client)
287 && string_match(tok, eval_user(request)));
288 }
289 }
290
291 /* host_match - match host name and/or address against pattern */
292
293 static int
294 host_match(char *tok, struct host_info *host)
295 {
296 char *mask;
297
298 /*
299 * This code looks a little hairy because we want to avoid unnecessary
300 * hostname lookups.
301 *
302 * The KNOWN pattern requires that both address AND name be known; some
303 * patterns are specific to host names or to host addresses; all other
304 * patterns are satisfied when either the address OR the name match.
305 */
306
307 if (tok[0] == '@') { /* netgroup: look it up */
308 #ifdef NETGROUP
309 static char *mydomain = 0;
310 if (mydomain == 0)
311 yp_get_default_domain(&mydomain);
312 return (innetgr(tok + 1, eval_hostname(host), NULL, mydomain));
313 #else
314 tcpd_warn("netgroup support is disabled"); /* not tcpd_jump() */
315 return (NO);
316 #endif
317 } else if (tok[0] == '/') { /* /file hack */
318 return (hostfile_match(tok, host));
319 } else if (STR_EQ(tok, "KNOWN")) { /* check address and name */
320 char *name = eval_hostname(host);
321 return (STR_NE(eval_hostaddr(host), unknown) && HOSTNAME_KNOWN(name));
322 } else if (STR_EQ(tok, "LOCAL")) { /* local: no dots in name */
323 char *name = eval_hostname(host);
324 return (strchr(name, '.') == 0 && HOSTNAME_KNOWN(name));
325 } else if (strncmp(tok, "{RBL}.", 6) == 0) { /* RBL lookup in domain */
326 return rbl_match(tok+6, eval_hostaddr(host));
327 } else if ((mask = split_at(tok, '/')) != 0) { /* net/mask */
328 return (masked_match(tok, mask, eval_hostaddr(host)));
329 } else { /* anything else */
330 return (string_match(tok, eval_hostaddr(host))
331 || (NOT_INADDR(tok) && string_match(tok, eval_hostname(host))));
332 }
333 }
334
335 /* hostfile_match - look up host patterns from file */
336
337 static int
338 hostfile_match(char *path, struct host_info *host)
339 {
340 char tok[512];
341 int match = NO;
342 FILE *fp;
343
344 if ((fp = fopen(path, "r")) != 0) {
345 while (fscanf(fp, "%511s", tok) == 1 && !(match = host_match(tok, host)))
346 /* void */ ;
347 fclose(fp);
348 } else if (errno != ENOENT) {
349 tcpd_warn("open %s: %m", path);
350 }
351 return (match);
352 }
353
354 /* rbl_match() - match host by looking up in RBL domain */
355
356 static int
357 rbl_match(
358 char *rbl_domain, /* RBL domain */
359 char *rbl_hostaddr) /* hostaddr */
360 {
361 char *rbl_name;
362 unsigned long host_address;
363 int ret = NO;
364 size_t len = strlen(rbl_domain) + (4 * 4) + 2;
365
366 if (dot_quad_addr(rbl_hostaddr, &host_address) != 0) {
367 tcpd_warn("unable to convert %s to address", rbl_hostaddr);
368 return (NO);
369 }
370 host_address = ntohl(host_address);
371 /* construct the rbl name to look up */
372 if ((rbl_name = malloc(len)) == NULL) {
373 tcpd_jump("not enough memory to build RBL name for %s in %s", rbl_hostaddr, rbl_domain);
374 /* NOTREACHED */
375 }
376 snprintf(rbl_name, len, "%u.%u.%u.%u.%s",
377 (unsigned int) ((host_address) & 0xff),
378 (unsigned int) ((host_address >> 8) & 0xff),
379 (unsigned int) ((host_address >> 16) & 0xff),
380 (unsigned int) ((host_address >> 24) & 0xff),
381 rbl_domain);
382 /* look it up */
383 if (gethostbyname(rbl_name) != NULL) {
384 /* successful lookup - they're on the RBL list */
385 ret = YES;
386 }
387 free(rbl_name);
388
389 return ret;
390 }
391
392 /* string_match - match string against pattern */
393
394 static int
395 string_match(char *tok, char *string)
396 {
397 int n;
398
399 if (tok[0] == '.') { /* suffix */
400 n = strlen(string) - strlen(tok);
401 return (n > 0 && STR_EQ(tok, string + n));
402 } else if (STR_EQ(tok, "ALL")) { /* all: match any */
403 return (YES);
404 } else if (STR_EQ(tok, "KNOWN")) { /* not unknown */
405 return (STR_NE(string, unknown));
406 } else if (tok[(n = strlen(tok)) - 1] == '.') { /* prefix */
407 return (STRN_EQ(tok, string, n));
408 } else { /* exact match */
409 return (STR_EQ(tok, string));
410 }
411 }
412
413 /* masked_match - match address against netnumber/netmask */
414
415 static int
416 masked_match(char *net_tok, char *mask_tok, char *string)
417 {
418 #ifndef INET6
419 return masked_match4(net_tok, mask_tok, string);
420 #else
421 /*
422 * masked_match4() is kept just for supporting shortened IPv4 address form.
423 * If we could get rid of shortened IPv4 form, we could just always use
424 * masked_match6().
425 */
426 if (dot_quad_addr(net_tok, NULL) != -1 &&
427 dot_quad_addr(mask_tok, NULL) != -1 &&
428 dot_quad_addr(string, NULL) != -1) {
429 return masked_match4(net_tok, mask_tok, string);
430 } else
431 return masked_match6(net_tok, mask_tok, string);
432 #endif
433 }
434
435 static int
436 masked_match4(char *net_tok, char *mask_tok, char *string)
437 {
438 unsigned long net;
439 unsigned long mask;
440 unsigned long addr;
441
442 /*
443 * Disallow forms other than dotted quad: the treatment that inet_addr()
444 * gives to forms with less than four components is inconsistent with the
445 * access control language. John P. Rouillard <rouilj (at) cs.umb.edu>.
446 */
447
448 if (dot_quad_addr(string, &addr) != 0)
449 return (NO);
450 if (dot_quad_addr(net_tok, &net) != 0 ||
451 dot_quad_addr(mask_tok, &mask) != 0) {
452 tcpd_warn("bad net/mask expression: %s/%s", net_tok, mask_tok);
453 return (NO); /* not tcpd_jump() */
454 }
455
456 if ((net & ~mask) != 0)
457 tcpd_warn("host bits not all zero in %s/%s", net_tok, mask_tok);
458
459 return ((addr & mask) == net);
460 }
461
462 #ifdef INET6
463 static int
464 masked_match6(char *net_tok, char *mask_tok, char *string)
465 {
466 union {
467 struct sockaddr sa;
468 struct sockaddr_in sin;
469 struct sockaddr_in6 sin6;
470 } net, mask, addr;
471 struct addrinfo hints, *res;
472 unsigned long masklen;
473 char *ep;
474 size_t i;
475 char *np, *mp, *ap;
476 size_t alen;
477
478 memset(&hints, 0, sizeof(hints));
479 hints.ai_family = PF_UNSPEC;
480 hints.ai_socktype = SOCK_DGRAM; /*dummy*/
481 hints.ai_flags = AI_NUMERICHOST;
482 if (getaddrinfo(net_tok, "0", &hints, &res) == 0) {
483 if (res->ai_addrlen > sizeof(net) || res->ai_next) {
484 freeaddrinfo(res);
485 return NO;
486 }
487 memcpy(&net, res->ai_addr, res->ai_addrlen);
488 freeaddrinfo(res);
489 } else
490 return NO;
491
492 memset(&hints, 0, sizeof(hints));
493 hints.ai_family = net.sa.sa_family;
494 hints.ai_socktype = SOCK_DGRAM; /*dummy*/
495 hints.ai_flags = AI_NUMERICHOST;
496 ep = NULL;
497 if (getaddrinfo(mask_tok, "0", &hints, &res) == 0) {
498 if (res->ai_family == AF_INET6 &&
499 ((struct sockaddr_in6 *)res->ai_addr)->sin6_scope_id) {
500 freeaddrinfo(res);
501 return NO;
502 }
503 if (res->ai_addrlen > sizeof(mask) || res->ai_next) {
504 freeaddrinfo(res);
505 return NO;
506 }
507 memcpy(&mask, res->ai_addr, res->ai_addrlen);
508 freeaddrinfo(res);
509 } else {
510 ep = NULL;
511 masklen = strtoul(mask_tok, &ep, 10);
512 if (ep && !*ep) {
513 memset(&mask, 0, sizeof(mask));
514 mask.sa.sa_family = net.sa.sa_family;
515 mask.sa.sa_len = net.sa.sa_len;
516 switch (mask.sa.sa_family) {
517 case AF_INET:
518 mp = (char *)&mask.sin.sin_addr;
519 alen = sizeof(mask.sin.sin_addr);
520 break;
521 case AF_INET6:
522 mp = (char *)&mask.sin6.sin6_addr;
523 alen = sizeof(mask.sin6.sin6_addr);
524 break;
525 default:
526 return NO;
527 }
528 if (masklen / 8 > alen)
529 return NO;
530 memset(mp, 0xff, masklen / 8);
531 if (masklen % 8)
532 mp[masklen / 8] = 0xff00 >> (masklen % 8);
533 } else
534 return NO;
535 }
536
537 memset(&hints, 0, sizeof(hints));
538 hints.ai_family = PF_UNSPEC;
539 hints.ai_socktype = SOCK_DGRAM; /*dummy*/
540 hints.ai_flags = AI_NUMERICHOST;
541 if (getaddrinfo(string, "0", &hints, &res) == 0) {
542 if (res->ai_addrlen > sizeof(addr) || res->ai_next) {
543 freeaddrinfo(res);
544 return NO;
545 }
546 /* special case - IPv4 mapped address */
547 if (net.sa.sa_family == AF_INET && res->ai_family == AF_INET6 &&
548 IN6_IS_ADDR_V4MAPPED(&((struct sockaddr_in6 *)res->ai_addr)->sin6_addr)) {
549 memset(&addr, 0, sizeof(addr));
550 addr.sa.sa_family = net.sa.sa_family;
551 addr.sa.sa_len = net.sa.sa_len;
552 memcpy(&addr.sin.sin_addr,
553 &((struct sockaddr_in6 *)res->ai_addr)->sin6_addr.s6_addr[12],
554 sizeof(addr.sin.sin_addr));
555 } else
556 memcpy(&addr, res->ai_addr, res->ai_addrlen);
557 freeaddrinfo(res);
558 } else
559 return NO;
560
561 if (net.sa.sa_family != mask.sa.sa_family ||
562 net.sa.sa_family != addr.sa.sa_family) {
563 return NO;
564 }
565
566 switch (net.sa.sa_family) {
567 case AF_INET:
568 np = (char *)&net.sin.sin_addr;
569 mp = (char *)&mask.sin.sin_addr;
570 ap = (char *)&addr.sin.sin_addr;
571 alen = sizeof(net.sin.sin_addr);
572 break;
573 case AF_INET6:
574 np = (char *)&net.sin6.sin6_addr;
575 mp = (char *)&mask.sin6.sin6_addr;
576 ap = (char *)&addr.sin6.sin6_addr;
577 alen = sizeof(net.sin6.sin6_addr);
578 break;
579 default:
580 return NO;
581 }
582
583 for (i = 0; i < alen; i++)
584 if (np[i] & ~mp[i]) {
585 tcpd_warn("host bits not all zero in %s/%s", net_tok, mask_tok);
586 break;
587 }
588
589 for (i = 0; i < alen; i++)
590 ap[i] &= mp[i];
591
592 if (addr.sa.sa_family == AF_INET6 && net.sin6.sin6_scope_id &&
593 addr.sin6.sin6_scope_id != net.sin6.sin6_scope_id)
594 return NO;
595 return (memcmp(ap, np, alen) == 0);
596 }
597 #endif
598