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