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