1/*	$NetBSD: tcpdchk.c,v 1.13 2018/01/23 21:06:26 sevan Exp $	*/
2
3 /*
4  * tcpdchk - examine all tcpd access control rules and inetd.conf entries
5  *
6  * Usage: tcpdchk [-a] [-d] [-i inet_conf] [-v]
7  *
8  * -a: complain about implicit "allow" at end of rule.
9  *
10  * -d: rules in current directory.
11  *
12  * -i: location of inetd.conf file.
13  *
14  * -v: show all rules.
15  *
16  * Author: Wietse Venema, Eindhoven University of Technology, The Netherlands.
17  */
18
19#include <sys/cdefs.h>
20#ifndef lint
21#if 0
22static char sccsid[] = "@(#) tcpdchk.c 1.8 97/02/12 02:13:25";
23#else
24__RCSID("$NetBSD: tcpdchk.c,v 1.13 2018/01/23 21:06:26 sevan Exp $");
25#endif
26#endif
27
28/* System libraries. */
29
30#include <sys/types.h>
31#include <sys/stat.h>
32#include <netinet/in.h>
33#include <arpa/inet.h>
34#include <stdio.h>
35#include <syslog.h>
36#include <setjmp.h>
37#include <errno.h>
38#include <netdb.h>
39#include <string.h>
40#include <stdlib.h>
41#include <unistd.h>
42
43#ifndef INADDR_NONE
44#define INADDR_NONE     (-1)		/* XXX should be 0xffffffff */
45#endif
46
47#ifndef S_ISDIR
48#define S_ISDIR(m)	(((m) & S_IFMT) == S_IFDIR)
49#endif
50
51/* Application-specific. */
52
53#include "tcpd.h"
54#include "inetcf.h"
55#include "scaffold.h"
56
57#ifdef NO_NETGRENT
58	/* SCO has no *netgrent() support */
59#else
60# ifdef NETGROUP
61#  include <netgroup.h>
62# endif
63#endif
64
65 /*
66  * Stolen from hosts_access.c...
67  */
68static const char sep[] = ", \t\n";
69
70#define	BUFLEN 2048
71
72int     resident = 0;
73int     hosts_access_verbose = 0;
74const char *hosts_allow_table = HOSTS_ALLOW;
75const char *hosts_deny_table = HOSTS_DENY;
76extern jmp_buf tcpd_buf;
77
78 /*
79  * Local stuff.
80  */
81static void usage(void);
82static void parse_table(const char *, struct request_info *);
83static void print_list(char *, char *);
84static void check_daemon_list(char *);
85static void check_client_list(char *);
86static void check_daemon(char *);
87static void check_user(char *);
88#ifdef INET6
89static int check_inet_addr(char *);
90#endif
91static int check_host(char *);
92static int reserved_name(char *);
93
94#define PERMIT	1
95#define DENY	0
96
97#define YES	1
98#define	NO	0
99
100static int defl_verdict;
101static char *myname;
102static int allow_check;
103static char *inetcf;
104
105int
106main(int argc, char **argv)
107{
108    struct request_info request;
109    struct stat st;
110    int     c;
111
112    myname = argv[0];
113
114    /*
115     * Parse the JCL.
116     */
117    while ((c = getopt(argc, argv, "adi:v")) != -1) {
118	switch (c) {
119	case 'a':
120	    allow_check = 1;
121	    break;
122	case 'd':
123	    hosts_allow_table = "hosts.allow";
124	    hosts_deny_table = "hosts.deny";
125	    break;
126	case 'i':
127	    inetcf = optarg;
128	    break;
129	case 'v':
130	    hosts_access_verbose++;
131	    break;
132	default:
133	    usage();
134	    /* NOTREACHED */
135	}
136    }
137    if (argc != optind)
138	usage();
139
140    /*
141     * When confusion really strikes...
142     */
143    if (check_path(REAL_DAEMON_DIR, &st) < 0) {
144	tcpd_warn("REAL_DAEMON_DIR %s: %m", REAL_DAEMON_DIR);
145    } else if (!S_ISDIR(st.st_mode)) {
146	tcpd_warn("REAL_DAEMON_DIR %s is not a directory", REAL_DAEMON_DIR);
147    }
148
149    /*
150     * Process the inet configuration file (or its moral equivalent). This
151     * information is used later to find references in hosts.allow/deny to
152     * unwrapped services, and other possible problems.
153     */
154    inetcf = inet_cfg(inetcf);
155    if (hosts_access_verbose)
156	printf("Using network configuration file: %s\n", inetcf);
157
158    /*
159     * These are not run from inetd but may have built-in access control.
160     */
161    inet_set("portmap", WR_NOT);
162    inet_set("rpcbind", WR_NOT);
163
164    /*
165     * Check accessibility of access control files.
166     */
167    (void) check_path(hosts_allow_table, &st);
168    (void) check_path(hosts_deny_table, &st);
169
170    /*
171     * Fake up an arbitrary service request.
172     */
173    request_init(&request,
174		 RQ_DAEMON, "daemon_name",
175		 RQ_SERVER_NAME, "server_hostname",
176		 RQ_SERVER_ADDR, "server_addr",
177		 RQ_USER, "user_name",
178		 RQ_CLIENT_NAME, "client_hostname",
179		 RQ_CLIENT_ADDR, "client_addr",
180		 RQ_FILE, 1,
181		 0);
182
183    /*
184     * Examine all access-control rules.
185     */
186    defl_verdict = PERMIT;
187    parse_table(hosts_allow_table, &request);
188    defl_verdict = DENY;
189    parse_table(hosts_deny_table, &request);
190    return (0);
191}
192
193/* usage - explain */
194
195static void
196usage(void)
197{
198    fprintf(stderr, "usage: %s [-a] [-d] [-i inet_conf] [-v]\n", myname);
199    fprintf(stderr, "	-a: report rules with implicit \"ALLOW\" at end\n");
200    fprintf(stderr, "	-d: use allow/deny files in current directory\n");
201    fprintf(stderr, "	-i: location of inetd.conf file\n");
202    fprintf(stderr, "	-v: list all rules\n");
203    exit(1);
204}
205
206/* parse_table - like table_match(), but examines _all_ entries */
207
208static void
209parse_table(const char *table, struct request_info *request)
210{
211    FILE   *fp;
212    volatile int     real_verdict;
213    char    sv_list[BUFLEN];		/* becomes list of daemons */
214    char   *cl_list;			/* becomes list of requests */
215    char   *sh_cmd;			/* becomes optional shell command */
216    int     verdict;
217    volatile struct tcpd_context saved_context;
218
219    saved_context = tcpd_context;		/* stupid compilers */
220
221    if ((fp = fopen(table, "r")) != NULL) {
222	tcpd_context.file = table;
223	tcpd_context.line = 0;
224	while (xgets(sv_list, sizeof(sv_list), fp)) {
225	    if (sv_list[strlen(sv_list) - 1] != '\n') {
226		tcpd_warn("missing newline or line too long");
227		continue;
228	    }
229	    if (sv_list[0] == '#' || sv_list[strspn(sv_list, " \t\r\n")] == 0)
230		continue;
231	    if ((cl_list = split_at(sv_list, ':')) == 0) {
232		tcpd_warn("missing \":\" separator");
233		continue;
234	    }
235	    sh_cmd = split_at(cl_list, ':');
236
237	    if (hosts_access_verbose)
238		printf("\n>>> Rule %s line %d:\n",
239		       tcpd_context.file, tcpd_context.line);
240
241	    if (hosts_access_verbose)
242		print_list("daemons:  ", sv_list);
243	    check_daemon_list(sv_list);
244
245	    if (hosts_access_verbose)
246		print_list("clients:  ", cl_list);
247	    check_client_list(cl_list);
248
249#ifdef PROCESS_OPTIONS
250	    real_verdict = defl_verdict;
251	    if (sh_cmd) {
252		verdict = setjmp(tcpd_buf);
253		if (verdict != 0) {
254		    real_verdict = (verdict == AC_PERMIT);
255		} else {
256		    dry_run = 1;
257		    process_options(sh_cmd, request);
258		    if (dry_run == 1 && real_verdict && allow_check)
259			tcpd_warn("implicit \"allow\" at end of rule");
260		}
261	    } else if (defl_verdict && allow_check) {
262		tcpd_warn("implicit \"allow\" at end of rule");
263	    }
264	    if (hosts_access_verbose)
265		printf("access:   %s\n", real_verdict ? "granted" : "denied");
266#else
267	    if (sh_cmd)
268		shell_cmd(percent_x(buf, sizeof(buf), sh_cmd, request));
269	    if (hosts_access_verbose)
270		printf("access:   %s\n", defl_verdict ? "granted" : "denied");
271#endif
272	}
273	(void) fclose(fp);
274    } else if (errno != ENOENT) {
275	tcpd_warn("cannot open %s: %m", table);
276    }
277    tcpd_context = saved_context;
278}
279
280/* print_list - pretty-print a list */
281
282static void print_list(char *title, char *list)
283{
284    char    buf[BUFLEN];
285    char   *cp;
286    char   *next;
287
288    fputs(title, stdout);
289    strlcpy(buf, list, sizeof(buf));
290
291    for (cp = strtok(buf, sep); cp != 0; cp = next) {
292	fputs(cp, stdout);
293	next = strtok((char *) 0, sep);
294	if (next != 0)
295	    fputs(" ", stdout);
296    }
297    fputs("\n", stdout);
298}
299
300/* check_daemon_list - criticize daemon list */
301
302static void check_daemon_list(char *list)
303{
304    char    buf[BUFLEN];
305    char   *cp;
306    char   *host;
307    int     daemons = 0;
308
309    strlcpy(buf, list, sizeof(buf));
310
311    for (cp = strtok(buf, sep); cp != 0; cp = strtok((char *) 0, sep)) {
312	if (STR_EQ(cp, "EXCEPT")) {
313	    daemons = 0;
314	} else {
315	    daemons++;
316	    if ((host = split_at(cp + 1, '@')) != 0 && check_host(host) > 1) {
317		tcpd_warn("host %s has more than one address", host);
318		tcpd_warn("(consider using an address instead)");
319	    }
320	    check_daemon(cp);
321	}
322    }
323    if (daemons == 0)
324	tcpd_warn("daemon list is empty or ends in EXCEPT");
325}
326
327/* check_client_list - criticize client list */
328
329static void check_client_list(char *list)
330{
331    char    buf[BUFLEN];
332    char   *cp;
333    char   *host;
334    int     clients = 0;
335#ifdef INET6
336    int l;
337#endif
338
339    strlcpy(buf, list, sizeof(buf));
340
341    for (cp = strtok(buf, sep); cp != 0; cp = strtok((char *) 0, sep)) {
342#ifdef INET6
343	l = strlen(cp);
344	if (cp[0] == '[' && cp[l - 1] == ']') {
345	    cp[l - 1] = '\0';
346	    cp++;
347	}
348#endif
349	if (STR_EQ(cp, "EXCEPT")) {
350	    clients = 0;
351	} else {
352	    clients++;
353	    if ((host = split_at(cp + 1, '@')) != NULL) {	/* user@host */
354		check_user(cp);
355		check_host(host);
356	    } else {
357		check_host(cp);
358	    }
359	}
360    }
361    if (clients == 0)
362	tcpd_warn("client list is empty or ends in EXCEPT");
363}
364
365/* check_daemon - criticize daemon pattern */
366
367static void check_daemon(char *pat)
368{
369    if (pat[0] == '@') {
370	tcpd_warn("%s: daemon name begins with \"@\"", pat);
371    } else if (pat[0] == '.') {
372	tcpd_warn("%s: daemon name begins with dot", pat);
373    } else if (pat[strlen(pat) - 1] == '.') {
374	tcpd_warn("%s: daemon name ends in dot", pat);
375    } else if (STR_EQ(pat, "ALL") || STR_EQ(pat, unknown)) {
376	 /* void */ ;
377    } else if (STR_EQ(pat, "FAIL")) {		/* obsolete */
378	tcpd_warn("FAIL is no longer recognized");
379	tcpd_warn("(use EXCEPT or DENY instead)");
380    } else if (reserved_name(pat)) {
381	tcpd_warn("%s: daemon name may be reserved word", pat);
382    } else {
383	switch (inet_get(pat)) {
384	case WR_UNKNOWN:
385	    tcpd_warn("%s: no such process name in %s", pat, inetcf);
386	    inet_set(pat, WR_YES);		/* shut up next time */
387	    break;
388	case WR_NOT:
389	    tcpd_warn("%s: service possibly not wrapped", pat);
390	    inet_set(pat, WR_YES);
391	    break;
392	}
393    }
394}
395
396/* check_user - criticize user pattern */
397
398static void check_user(char *pat)
399{
400    if (pat[0] == '@') {			/* @netgroup */
401	tcpd_warn("%s: user name begins with \"@\"", pat);
402    } else if (pat[0] == '.') {
403	tcpd_warn("%s: user name begins with dot", pat);
404    } else if (pat[strlen(pat) - 1] == '.') {
405	tcpd_warn("%s: user name ends in dot", pat);
406    } else if (STR_EQ(pat, "ALL") || STR_EQ(pat, unknown)
407	       || STR_EQ(pat, "KNOWN")) {
408	 /* void */ ;
409    } else if (STR_EQ(pat, "FAIL")) {		/* obsolete */
410	tcpd_warn("FAIL is no longer recognized");
411	tcpd_warn("(use EXCEPT or DENY instead)");
412    } else if (reserved_name(pat)) {
413	tcpd_warn("%s: user name may be reserved word", pat);
414    }
415}
416
417#ifdef INET6
418static int check_inet_addr(char *pat)
419{
420	struct addrinfo *res;
421
422	res = find_inet_addr(pat, AI_NUMERICHOST);
423	if (res) {
424		freeaddrinfo(res);
425		return 1;
426	} else
427		return 0;
428}
429#endif
430
431/* check_host - criticize host pattern */
432static int check_host(char *pat)
433{
434    char   *mask;
435    int     addr_count = 1;
436
437    if (pat[0] == '@') {			/* @netgroup */
438#ifdef NO_NETGRENT
439	/* SCO has no *netgrent() support */
440#else
441#ifdef NETGROUP
442	const char   *machinep;
443	const char   *userp;
444	const char   *domainp;
445
446	setnetgrent(pat + 1);
447	if (getnetgrent(&machinep, &userp, &domainp) == 0)
448	    tcpd_warn("%s: unknown or empty netgroup", pat + 1);
449	endnetgrent();
450#else
451	tcpd_warn("netgroup support disabled");
452#endif
453#endif
454    } else if ((mask = split_at(pat, '/')) != NULL) {	/* network/netmask */
455#ifdef INET6
456	char *ep;
457#endif
458	if (dot_quad_addr(pat, NULL) != INADDR_NONE
459	    || dot_quad_addr(mask, NULL) != INADDR_NONE)
460	    ; /*okay*/
461#ifdef INET6
462	else if (check_inet_addr(pat) && check_inet_addr(mask))
463	    ; /*okay*/
464	else if (check_inet_addr(pat) &&
465	    (ep = NULL, strtoul(mask, &ep, 10), ep && !*ep))
466	    ; /*okay*/
467#endif
468	else
469	    tcpd_warn("%s/%s: bad net/mask pattern", pat, mask);
470    } else if (STR_EQ(pat, "FAIL")) {		/* obsolete */
471	tcpd_warn("FAIL is no longer recognized");
472	tcpd_warn("(use EXCEPT or DENY instead)");
473    } else if (reserved_name(pat)) {		/* other reserved */
474	 /* void */ ;
475    } else if (NOT_INADDR(pat)) {		/* internet name */
476	if (pat[strlen(pat) - 1] == '.') {
477	    tcpd_warn("%s: domain or host name ends in dot", pat);
478	} else if (pat[0] != '.') {
479	    addr_count = check_dns(pat);
480	}
481    } else {					/* numeric form */
482	if (STR_EQ(pat, "0.0.0.0") || STR_EQ(pat, "255.255.255.255")) {
483	    /* void */ ;
484	} else if (pat[0] == '.') {
485	    tcpd_warn("%s: network number begins with dot", pat);
486	} else if (pat[strlen(pat) - 1] != '.') {
487	    check_dns(pat);
488	}
489    }
490    return (addr_count);
491}
492
493/* reserved_name - determine if name is reserved */
494
495static int reserved_name(char *pat)
496{
497    return (STR_EQ(pat, unknown)
498	    || STR_EQ(pat, "KNOWN")
499	    || STR_EQ(pat, paranoid)
500	    || STR_EQ(pat, "ALL")
501	    || STR_EQ(pat, "LOCAL"));
502}
503