chiark / gitweb /
b621f450d099858ac7167578bb32557a6a28beb2
[fwd] / fwd.c
1 /* -*-c-*-
2  *
3  * Port forwarding thingy
4  *
5  * (c) 1999 Straylight/Edgeware
6  */
7
8 /*----- Licensing notice --------------------------------------------------*
9  *
10  * This file is part of the `fwd' port forwarder.
11  *
12  * `fwd' is free software; you can redistribute it and/or modify
13  * it under the terms of the GNU General Public License as published by
14  * the Free Software Foundation; either version 2 of the License, or
15  * (at your option) any later version.
16  *
17  * `fwd' is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20  * GNU General Public License for more details.
21  *
22  * You should have received a copy of the GNU General Public License
23  * along with `fwd'; if not, write to the Free Software Foundation,
24  * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25  */
26
27 #include "fwd.h"
28
29 /*----- Global variables --------------------------------------------------*/
30
31 sel_state *sel;                         /* Multiplexor for nonblocking I/O */
32
33 /*----- Static variables --------------------------------------------------*/
34
35 typedef struct conffile {
36   struct conffile *next;
37   char *name;
38 } conffile;
39
40 static unsigned flags = 0;              /* Global state flags */
41 static unsigned active = 0;             /* Number of active things */
42 static conffile *conffiles = 0;         /* List of configuration files */
43
44 #define FW_SYSLOG 1u
45 #define FW_QUIET 2u
46 #define FW_SET 4u
47
48 /*----- Configuration parsing ---------------------------------------------*/
49
50 /* --- @parse@ --- *
51  *
52  * Arguments:   @scanner *sc@ = pointer to scanner definition
53  *
54  * Returns:     ---
55  *
56  * Use:         Parses a configuration file from the scanner.
57  */
58
59 static source_ops *sources[] =
60   { &xsource_ops, &fsource_ops, &ssource_ops, 0 };
61 static target_ops *targets[] =
62   { &xtarget_ops, &ftarget_ops, &starget_ops, 0 };
63
64 void parse(scanner *sc)
65 {
66   token(sc);
67
68   for (;;) {
69     if (sc->t == CTOK_EOF)
70       break;
71     if (sc->t != CTOK_WORD)
72       error(sc, "parse error, keyword expected");
73
74     /* --- Handle a forwarding request --- */
75
76     if (strcmp(sc->d.buf, "forward") == 0 ||
77         strcmp(sc->d.buf, "fwd") == 0 ||
78         strcmp(sc->d.buf, "from") == 0) {
79       source *s;
80       target *t;
81
82       token(sc);
83
84       /* --- Read a source description --- */
85
86       {
87         source_ops **sops;
88
89         /* --- Try to find a source type which understands --- */
90
91         s = 0;
92         for (sops = sources; *sops; sops++) {
93           if ((s = (*sops)->read(sc)) != 0)
94             goto found_source;
95         }
96         error(sc, "unknown source name `%s'", sc->d.buf);
97
98         /* --- Read any source-specific options --- */
99
100       found_source:
101         if (sc->t == '{') {
102           token(sc);
103           while (sc->t == CTOK_WORD) {
104             if (!s->ops->option || !s->ops->option(s, sc)) {
105               error(sc, "unknown %s source option `%s'",
106                     s->ops->name, sc->d.buf);
107             }
108             if (sc->t == ';')
109               token(sc);
110           }
111           if (sc->t != '}')
112             error(sc, "parse error, missing `}'");
113           token(sc);
114         }
115       }
116
117       /* --- Read a destination description --- */
118
119       if (sc->t == CTOK_WORD && (strcmp(sc->d.buf, "to") == 0 ||
120                                  strcmp(sc->d.buf, "->") == 0))
121         token(sc);
122
123       {
124         target_ops **tops;
125
126         /* --- Try to find a target which understands --- */
127
128         t = 0;
129         for (tops = targets; *tops; tops++) {
130           if ((t = (*tops)->read(sc)) != 0)
131             goto found_target;
132         }
133         error(sc, "unknown target name `%s'", sc->d.buf);
134
135         /* --- Read any target-specific options --- */
136
137       found_target:
138         if (sc->t == '{') {
139           token(sc);
140           while (sc->t == CTOK_WORD) {
141             if (!t->ops->option || !t->ops->option(t, sc)) {
142               error(sc, "unknown %s target option `%s'",
143                     t->ops->name, sc->d.buf);
144             }
145             if (sc->t == ';')
146               token(sc);
147           }
148           if (sc->t != '}')
149             error(sc, "parse error, `}' expected");
150           token(sc);
151         }
152       }
153
154       /* --- Combine the source and target --- */
155
156       s->ops->attach(s, sc, t);
157       if (t->ops->confirm)
158         t->ops->confirm(t);
159     }
160
161     /* --- Include configuration from a file --- *
162      *
163      * Slightly tricky.  Scan the optional semicolon from the including
164      * stream, not the included one.
165      */
166
167     else if (strcmp(sc->d.buf, "include") == 0) {
168       FILE *fp;
169       dstr d = DSTR_INIT;
170
171       token(sc);
172       conf_name(sc, '/', &d);
173       if ((fp = fopen(d.buf, "r")) == 0)
174         error(sc, "can't include `%s': %s", d.buf, strerror(errno));
175       if (sc->t == ';')
176         token(sc);
177       pushback(sc);
178       scan_push(sc, scan_file(fp, d.buf, 0));
179       token(sc);
180       dstr_destroy(&d);
181       continue;                         /* Don't parse a trailing `;' */
182     }
183
184     /* --- Other configuration is handled elsewhere --- */
185
186     else {
187
188       /* --- First try among the sources --- */
189
190       {
191         source_ops **sops;
192
193         for (sops = sources; *sops; sops++) {
194           if ((*sops)->option && (*sops)->option(0, sc))
195             goto found_option;
196         }
197       }
198
199       /* --- Then try among the targets --- */
200
201       {
202         target_ops **tops;
203
204         for (tops = targets; *tops; tops++) {
205           if ((*tops)->option && (*tops)->option(0, sc))
206             goto found_option;
207         }
208       }
209
210       /* --- Nobody wants the option --- */
211
212       error(sc, "unknown global option or prefix `%s'", sc->d.buf);
213
214     found_option:;
215     }
216
217     if (sc->t == ';')
218       token(sc);
219   }
220 }
221
222 /*----- General utility functions -----------------------------------------*/
223
224 /* --- @fw_log@ --- *
225  *
226  * Arguments:   @time_t t@ = when the connection occurred or (@-1@)
227  *              @const char *fmt@ = format string to fill in
228  *              @...@ = other arguments
229  *
230  * Returns:     ---
231  *
232  * Use:         Logs a connection.
233  */
234
235 void fw_log(time_t t, const char *fmt, ...)
236 {
237   struct tm *tm;
238   dstr d = DSTR_INIT;
239   va_list ap;
240
241   if (flags & FW_QUIET)
242     return;
243
244   if (t == NOW)
245     t = time(0);
246   tm = localtime(&t);
247   DENSURE(&d, 64);
248   d.len += strftime(d.buf, d.sz, "%Y-%m-%d %H:%M:%S ", tm);
249   va_start(ap, fmt);
250   dstr_vputf(&d, fmt, &ap);
251   va_end(ap);
252   if (flags & FW_SYSLOG)
253     syslog(LOG_NOTICE, "%s", d.buf);
254   else {
255     DPUTC(&d, '\n');
256     dstr_write(&d, stderr);
257   }
258   DDESTROY(&d);
259 }
260
261 /* --- @fw_inc@, @fw_dec@ --- *
262  *
263  * Arguments:   ---
264  *
265  * Returns:     ---
266  *
267  * Use:         Increments or decrements the active thing count.  `fwd' won't
268  *              quit while there are active things.
269  */
270
271 void fw_inc(void) { flags |= FW_SET; active++; }
272 void fw_dec(void) { if (active) active--; }
273
274 /* --- @fw_exit@ --- *
275  *
276  * Arguments:   ---
277  *
278  * Returns:     ---
279  *
280  * Use:         Exits when appropriate.
281  */
282
283 static void fw_exit(void)
284 {
285   endpt_killall();
286   source_killall();
287 }
288
289 /* --- @fw_tidy@ --- *
290  *
291  * Arguments:   @int n@ = signal number
292  *              @void *p@ = an uninteresting argument
293  *
294  * Returns:     ---
295  *
296  * Use:         Handles various signals and causes a clean tidy-up.
297  */
298
299 static void fw_tidy(int n, void *p)
300 {
301   const char *sn = 0;
302   switch (n) {
303     case SIGTERM: sn = "SIGTERM"; break;
304     case SIGINT: sn = "SIGINT"; break;
305     default: abort();
306   }
307
308   fw_log(NOW, "closing down gracefully on %s", sn);
309   source_killall();
310 }
311
312 /* --- @fw_die@ --- *
313  *
314  * Arguments:   @int n@ = signal number
315  *              @void *p@ = an uninteresting argument
316  *
317  * Returns:     ---
318  *
319  * Use:         Handles various signals and causes an abrupt shutdown.
320  */
321
322 static void fw_die(int n, void *p)
323 {
324   const char *sn = 0;
325   switch (n) {
326     case SIGQUIT: sn = "SIGQUIT"; break;
327     default: abort();
328   }
329
330   fw_log(NOW, "closing down abruptly on %s", sn);
331   source_killall();
332   endpt_killall();
333 }
334
335 /* --- @fw_reload@ --- *
336  *
337  * Arguments:   @int n@ = a signal number
338  *              @void *p@ = an uninteresting argument
339  *
340  * Returns:     ---
341  *
342  * Use:         Handles a hangup signal by re-reading configuration files.
343  */
344
345 static void fw_reload(int n, void *p)
346 {
347   FILE *fp;
348   scanner sc;
349   conffile *cf;
350
351   assert(n == SIGHUP);
352   if (!conffiles) {
353     fw_log(NOW, "no configuration files to reload: ignoring SIGHUP");
354     return;
355   }
356   fw_log(NOW, "reloading configuration files...");
357   source_killall();
358   scan_create(&sc);
359   for (cf = conffiles; cf; cf = cf->next) {
360     if ((fp = fopen(cf->name, "r")) == 0)
361       fw_log(NOW, "error loading `%s': %s", cf->name, strerror(errno));
362     else
363       scan_add(&sc, scan_file(fp, cf->name, 0));
364   }
365   parse(&sc);
366   fw_log(NOW, "... reload completed OK");
367 }
368
369 /*----- Startup and options parsing ---------------------------------------*/
370
371 /* --- Standard GNU help options --- */
372
373 static void version(FILE *fp)
374 {
375   pquis(fp, "$ version " VERSION "\n");
376 }
377
378 static void usage(FILE *fp)
379 {
380   pquis(fp, "Usage: $ [-dql] [-p PIDFILE] [-f FILE] [CONFIG-STMTS...]\n");
381 }
382
383 static void help(FILE *fp)
384 {
385   version(fp);
386   fputc('\n', fp);
387   usage(fp);
388   pquis(fp, "\n\
389 An excessively full-featured port-forwarder, which subsumes large chunks\n\
390 of the functionality of inetd, netcat, and normal cat.\n\
391 \n\
392 Configuration may be supplied in one or more configuration files, or on\n\
393 the command line (or both).  If no `-f' option is present, and no\n\
394 configuration is given on the command line, the standard input stream is\n\
395 read.\n\
396 \n\
397 Configuration is free-form.  Comments begin with a `#' character and\n\
398 continue to the end of the line.  Each command line argument is considered\n\
399 to be a separate line.\n\
400 \n\
401 The grammar is fairly complicated.  For a summary, run `$ --grammar'.\n\
402 For a summary of the various options, run `$ --options'.\n\
403 \n\
404 Options available are:\n\
405 \n\
406 Help options:\n\
407   -h, --help           Display this help message.\n\
408   -v, --version        Display the program's version number.\n\
409   -u, --usage          Display a terse usage summary.\n\
410 \n\
411 Configuration summary:\n\
412   -G, --grammar        Show a summary of the configuration language.\n\
413   -O, --options        Show a summary of the source and target options.\n\
414 \n\
415 Other options:\n\
416   -f, --file=FILE      Read configuration from a file.\n\
417   -q, --quiet          Don't emit any logging information.\n\
418   -d, --daemon         Fork into background after initializing.\n\
419   -p, --pidfile=FILE   Write process-id to the named FILE.\n\
420   -l, --syslog         Send log output to the system logger.\n\
421   -s, --setuid=USER    Change uid to USER after initializing sources.\n\
422   -g, --setgid=GRP     Change gid to GRP after initializing sources.\n\
423 ");
424 }
425
426 /* --- Other helpful options --- */
427
428 static void grammar(FILE *fp)
429 {
430   version(fp);
431   fputs("\nGrammar summary\n\n", fp);
432   fputs(grammar_text, fp);
433 }
434
435 static void options(FILE *fp)
436 {
437   version(fp);
438   fputs("\nOption summary\n\n", fp);
439   fputs(option_text, fp);
440 }
441
442 /* --- @main@ --- *
443  *
444  * Arguments:   @int argc@ = number of command line arguments
445  *              @char *argv[]@ = vector of argument strings
446  *
447  * Returns:     ---
448  *
449  * Use:         Simple port-forwarding server.
450  */
451
452 int main(int argc, char *argv[])
453 {
454   unsigned f = 0;
455   sel_state sst;
456   sig s_term, s_quit, s_int, s_hup;
457   scanner sc;
458   uid_t drop = -1;
459   gid_t dropg = -1;
460   const char *pidfile = 0;
461   conffile *cf, **cff = &conffiles;
462
463 #define f_bogus 1u
464 #define f_file 2u
465 #define f_syslog 4u
466 #define f_fork 8u
467
468   /* --- Initialize things --- */
469
470   ego(argv[0]);
471   sel = &sst;
472   sel_init(sel);
473   sub_init();
474   sig_init(sel);
475   bres_init(sel);
476   bres_exec(0);
477   exec_init();
478   fattr_init(&fattr_global);
479   scan_create(&sc);
480
481   atexit(fw_exit);
482
483   /* --- Parse command line options --- */
484
485   for (;;) {
486     static struct option opts[] = {
487
488       /* --- Standard GNU help options --- */
489
490       { "help",         0,              0,      'h' },
491       { "version",      0,              0,      'v' },
492       { "usage",        0,              0,      'u' },
493
494       /* --- Other help options --- */
495
496       { "grammar",      0,              0,      'G' },
497       { "options",      0,              0,      'O' },
498
499       /* --- Other useful arguments --- */
500
501       { "file",         OPTF_ARGREQ,    0,      'f' },
502       { "fork",         0,              0,      'd' },
503       { "daemon",       0,              0,      'd' },
504       { "pidfile",      OPTF_ARGREQ,    0,      'p' },
505       { "syslog",       0,              0,      'l' },
506       { "log",          0,              0,      'l' },
507       { "quiet",        0,              0,      'q' },
508       { "setuid",       OPTF_ARGREQ,    0,      's' },
509       { "uid",          OPTF_ARGREQ,    0,      's' },
510       { "setgid",       OPTF_ARGREQ,    0,      'g' },
511       { "gid",          OPTF_ARGREQ,    0,      'g' },
512
513       /* --- Magic terminator --- */
514
515       { 0,              0,              0,      0 }
516     };
517     int i = mdwopt(argc, argv, "+hvu" "GO" "f:dp:lqs:g:", opts, 0, 0, 0);
518
519     if (i < 0)
520       break;
521     switch (i) {
522       case 'h':
523         help(stdout);
524         exit(0);
525         break;
526       case 'v':
527         version(stdout);
528         exit(0);
529         break;
530       case 'u':
531         usage(stdout);
532         exit(0);
533         break;
534       case 'G':
535         grammar(stdout);
536         exit(0);
537         break;
538       case 'O':
539         options(stdout);
540         exit(0);
541         break;
542       case 'f':
543         if (strcmp(optarg, "-") == 0)
544           scan_add(&sc, scan_file(stdin, "<stdin>", SCF_NOCLOSE));
545         else {
546           FILE *fp;
547           if ((fp = fopen(optarg, "r")) == 0)
548             die(1, "couldn't open file `%s': %s", optarg, strerror(errno));
549           cf = CREATE(conffile);
550           cf->name = optarg;
551           *cff = cf;
552           cff = &cf->next;
553           scan_add(&sc, scan_file(fp, optarg, 0));
554         }
555         f |= f_file;
556         break;
557       case 'd':
558         f |= f_fork;
559         break;
560       case 'p':
561         pidfile = optarg;
562         break;
563       case 'l':
564         f |= f_syslog;
565         break;
566       case 'q':
567         flags |= FW_QUIET;
568         break;
569       case 's':
570         if (isdigit((unsigned char )optarg[0])) {
571           char *q;
572           drop = strtol(optarg, &q, 0);
573           if (*q)
574             die(1, "bad uid `%s'", optarg);
575         } else {
576           struct passwd *pw = getpwnam(optarg);
577           if (!pw)
578             die(1, "unknown user `%s'", optarg);
579           drop = pw->pw_uid;
580         }
581         break;
582       case 'g':
583         if (isdigit((unsigned char )optarg[0])) {
584           char *q;
585           dropg = strtol(optarg, &q, 0);
586           if (*q)
587             die(1, "bad gid `%s'", optarg);
588         } else {
589           struct group *gr = getgrnam(optarg);
590           if (!gr)
591             die(1, "unknown group `%s'", optarg);
592           dropg = gr->gr_gid;
593         }
594         break;
595       default:
596         f |= f_bogus;
597         break;
598     }
599   }
600
601   if (f & f_bogus) {
602     usage(stderr);
603     exit(1);
604   }
605   *cff = 0;
606
607   /* --- Deal with the remaining arguments --- */
608
609   if (optind < argc)
610     scan_add(&sc, scan_argv(argv + optind));
611   else if (f & f_file)
612     /* Cool */;
613   else if (!isatty(STDIN_FILENO))
614     scan_add(&sc, scan_file(stdin, "<stdin>", SCF_NOCLOSE));
615   else {
616     moan("no configuration given and stdin is a terminal.");
617     moan("type `%s --help' for usage information.", QUIS);
618     exit(1);
619   }
620
621   /* --- Parse the configuration now gathered --- */
622
623   parse(&sc);
624
625   /* --- Set up some signal handlers --- *
626    *
627    * Don't enable @SIGINT@ if the caller already disabled it.
628    */
629
630   {
631     struct sigaction sa;
632
633     sig_add(&s_term, SIGTERM, fw_tidy, 0);
634     sig_add(&s_quit, SIGQUIT, fw_die, 0);
635     sigaction(SIGINT, 0, &sa);
636     if (sa.sa_handler != SIG_IGN)
637       sig_add(&s_int, SIGINT, fw_tidy, 0);
638     sig_add(&s_hup, SIGHUP, fw_reload, 0);
639   }
640
641   /* --- Fork into the background --- */
642
643   if (f & f_fork) {
644     pid_t kid;
645
646     kid = fork();
647     if (kid == -1)
648       die(1, "couldn't fork: %s", strerror(errno));
649     if (kid != 0)
650       _exit(0);
651
652     close(0); close(1); close(2);
653     if (chdir("/"))
654       die(1, "couldn't change to root directory: %s", strerror(errno));
655     setsid();
656
657     kid = fork();
658     if (kid != 0)
659       _exit(0);
660   }
661   if (pidfile) {
662     FILE *fp = fopen(pidfile, "w");
663     if (!fp) {
664       die(EXIT_FAILURE, "couldn't create pidfile `%s': %s",
665           pidfile, strerror(errno));
666     }
667     fprintf(fp, "%lu\n", (unsigned long)getpid());
668     if (fflush(fp) || ferror(fp) || fclose(fp)) {
669       die(EXIT_FAILURE, "couldn't write pidfile `%s': %s",
670           pidfile, strerror(errno));
671     }
672   }
673
674   if (f & f_syslog) {
675     flags |= FW_SYSLOG;
676     openlog(QUIS, 0, LOG_DAEMON);
677   }
678
679   /* --- Drop privileges --- */
680
681   if (drop != (uid_t)-1)
682     privconn_split(sel);
683 #ifdef HAVE_SETGROUPS
684   if ((dropg != (gid_t)-1 && (setgid(dropg) || setgroups(1, &dropg))) ||
685       (drop != (uid_t)-1 && setuid(drop)))
686     die(1, "couldn't drop privileges: %s", strerror(errno));
687 #else
688   if ((dropg != (gid_t)-1 && setgid(dropg)) ||
689       (drop != (uid_t)-1 && setuid(drop)))
690     die(1, "couldn't drop privileges: %s", strerror(errno));
691 #endif
692
693   /* --- Let rip --- */
694
695   if (!(flags & FW_SET))
696     moan("nothing to do!");
697   signal(SIGPIPE, SIG_IGN);
698
699   {
700     int selerr = 0;
701     while (active) {
702       if (!sel_select(sel))
703         selerr = 0;
704       else if (errno != EINTR && errno != EAGAIN) {
705         fw_log(NOW, "error from select: %s", strerror(errno));
706         selerr++;
707         if (selerr > 8) {
708           fw_log(NOW, "too many consecutive select errors: bailing out");
709           exit(EXIT_FAILURE);
710         }
711       }
712     }
713   }
714
715   return (0);
716 }
717
718 /*----- That's all, folks -------------------------------------------------*/