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