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