chiark / gitweb /
linux.c: Reformat to suppress a compiler warning.
[yaid] / yaid.c
diff --git a/yaid.c b/yaid.c
index cde206991a11785b938fa30bd71db0bca596bcc6..d0d917aa2e4b8eab6bfc1f29c36c6bfee554cde0 100644 (file)
--- a/yaid.c
+++ b/yaid.c
@@ -55,10 +55,13 @@ struct listen {
 struct client {
   int fd;                              /* The connection to the client */
   selbuf b;                            /* Accumulate lines of input */
+  union addr raddr;                    /* Remote address */
   struct query q;                      /* The clients query and our reply */
+  struct sel_timer t;                  /* Timeout for idle or doomed conn */
   struct listen *l;                    /* Back to the listener (and ops) */
   struct writebuf wb;                  /* Write buffer for our reply */
   struct proxy *px;                    /* Proxy if conn goes via NAT */
+  struct client *next;                 /* Next in a chain of clients */
 };
 
 /* A proxy connection. */
@@ -69,19 +72,29 @@ struct proxy {
   selbuf b;                            /* Accumulate the response line */
   struct writebuf wb;                  /* Write buffer for query */
   char nat[ADDRLEN];                   /* Server address, as text */
+  struct proxy *next;                  /* Next in a chain of proxies */
 };
 
 /*----- Static variables --------------------------------------------------*/
 
 static sel_state sel;                  /* I/O multiplexer state */
 
+static const char *pidfile = 0;                /* Where to write daemon's pid */
+
+static const char *policyfile = POLICYFILE; /* Filename for global policy */
 static const struct policy default_policy = POLICY_INIT(A_NAME);
 static policy_v policy = DA_INIT;      /* Vector of global policy rules */
 static fwatch polfw;                   /* Watch policy file for changes */
 
 static unsigned char tokenbuf[4096];   /* Random-ish data for tokens */
 static size_t tokenptr = sizeof(tokenbuf); /* Current read position */
-static int randfd;                     /* File descriptor for random data */
+
+static struct client *dead_clients = 0;        /* List of defunct clients */
+static struct proxy *dead_proxies = 0; /* List of defunct proxies */
+
+static unsigned flags = 0;             /* Various interesting flags */
+#define F_SYSLOG 1u                    /*   Use syslog for logging */
+#define F_RUNNING 2u                   /*   Running properly now */
 
 /*----- Ident protocol parsing --------------------------------------------*/
 
@@ -209,27 +222,59 @@ static void init_writebuf(struct writebuf *wb,
 
 /*----- General utilities -------------------------------------------------*/
 
-/* Format and log MSG somewhere sensible, at the syslog(3) priority PRIO.
- * Prefix it with a description of the query Q, if non-null.
- */
-void logmsg(const struct query *q, int prio, const char *msg, ...)
+static void vlogmsg(const struct query *q, int prio,
+                   const char *msg, va_list *ap)
 {
-  va_list ap;
   dstr d = DSTR_INIT;
+  time_t t;
+  struct tm *tm;
+  char buf[64];
 
-  va_start(ap, msg);
   if (q) {
     dputsock(&d, q->ao, &q->s[L]);
     dstr_puts(&d, " <-> ");
     dputsock(&d, q->ao, &q->s[R]);
     dstr_puts(&d, ": ");
   }
-  dstr_vputf(&d, msg, &ap);
-  va_end(ap);
-  fprintf(stderr, "yaid: %s\n", d.buf);
+  dstr_vputf(&d, msg, ap);
+
+  if (!(flags & F_RUNNING))
+    moan("%s", d.buf);
+  else if (flags & F_SYSLOG)
+    syslog(prio, "%s", d.buf);
+  else {
+    t = time(0);
+    tm = localtime(&t);
+    strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S %z", tm);
+    fprintf(stderr, "%s %s: %s\n", buf, QUIS, d.buf);
+  }
+
   dstr_destroy(&d);
 }
 
+/* Format and log MSG somewhere sensible, at the syslog(3) priority PRIO.
+ * Prefix it with a description of the query Q, if non-null.
+ */
+void logmsg(const struct query *q, int prio, const char *msg, ...)
+{
+  va_list ap;
+
+  va_start(ap, msg);
+  vlogmsg(q, prio, msg, &ap);
+  va_end(ap);
+}
+
+/* Format and report MSG as a fatal error, and exit. */
+void fatal(const char *msg, ...)
+{
+  va_list ap;
+
+  va_start(ap, msg);
+  vlogmsg(0, LOG_CRIT, msg, &ap);
+  va_end(ap);
+  exit(1);
+}
+
 /* Fix up a socket FD so that it won't bite us.  Returns zero on success, or
  * nonzero on error.
  */
@@ -275,7 +320,8 @@ static void done_client_write(int err, void *p)
 /* Format the message FMT and queue it to be sent to the client.  Client
  * input will be disabled until the write completes.
  */
-static void write_to_client(struct client *c, const char *fmt, ...)
+static void PRINTF_LIKE(2, 3)
+  write_to_client(struct client *c, const char *fmt, ...)
 {
   va_list ap;
   char buf[WRBUFSZ];
@@ -338,12 +384,28 @@ static void cancel_proxy(struct proxy *px)
     conn_kill(&px->cn);
   else {
     close(px->fd);
-    selbuf_destroy(&px->b);
-    free_writebuf(&px->wb);
+    selbuf_disable(&px->b);
   }
-  selbuf_enable(&px->c->b);
   px->c->px = 0;
-  xfree(px);
+  selbuf_enable(&px->c->b);
+  px->next = dead_proxies;
+  dead_proxies = px;
+}
+
+/* Delayed destruction of unsafe parts of proxies. */
+static void reap_dead_proxies(void)
+{
+  struct proxy *px, *pp;
+
+  for (px = dead_proxies; px; px = pp) {
+    pp = px->next;
+    if (px->fd != -1) {
+      selbuf_destroy(&px->b);
+      free_writebuf(&px->wb);
+    }
+    xfree(px);
+  }
+  dead_proxies = 0;
 }
 
 /* Notification that a line (presumably a reply) has been received from the
@@ -519,11 +581,49 @@ err_0:
 /* Disconnect a client, freeing up any associated resources. */
 static void disconnect_client(struct client *c)
 {
+  selbuf_disable(&c->b);
   close(c->fd);
-  selbuf_destroy(&c->b);
+  sel_rmtimer(&c->t);
   free_writebuf(&c->wb);
   if (c->px) cancel_proxy(c->px);
-  xfree(c);
+  c->next = dead_clients;
+  dead_clients = c;
+}
+
+/* Throw away dead clients now that we've reached a safe point in the
+ * program.
+ */
+static void reap_dead_clients(void)
+{
+  struct client *c, *cc;
+  for (c = dead_clients; c; c = cc) {
+    cc = c->next;
+    selbuf_destroy(&c->b);
+    xfree(c);
+  }
+  dead_clients = 0;
+}
+
+/* Time out a client because it's been idle for too long. */
+static void timeout_client(struct timeval *tv, void *p)
+{
+  struct client *c = p;
+  logmsg(&c->q, LOG_NOTICE, "timing out idle or stuck client");
+  sel_addtimer(&sel, &c->t, tv, timeout_client, 0);
+  disconnect_client(c);
+}
+
+/* Reset the client idle timer, as a result of activity.  Set EXISTP if
+ * there is an existing timer which needs to be removed.
+ */
+static void reset_client_timer(struct client *c, int existp)
+{
+  struct timeval tv;
+
+  gettimeofday(&tv, 0);
+  tv.tv_sec += 30;
+  if (existp) sel_rmtimer(&c->t);
+  sel_addtimer(&sel, &c->t, &tv, timeout_client, c);
 }
 
 /* Write a pseudorandom token into the buffer at P, which must have space for
@@ -543,8 +643,7 @@ static void user_token(char *p)
    * from the kernel.
    */
   if (tokenptr + TOKENRANDSZ >= sizeof(tokenbuf)) {
-    if (read(randfd, tokenbuf, sizeof(tokenbuf)) < sizeof(tokenbuf))
-      die(1, "unexpected short read or error from `/dev/urandom'");
+    fill_random(tokenbuf, sizeof(tokenbuf));
     tokenptr = 0;
   }
 
@@ -583,21 +682,25 @@ static void client_line(char *line, size_t len, void *p)
   struct policy upol = POLICY_INIT(A_LIMIT);
   struct policy_file pf;
   char buf[16];
-  int i;
+  int i, t;
 
   /* If the connection has closed, then tidy stuff away. */
+  c->q.s[R].addr = c->raddr;
   c->q.s[L].port = c->q.s[R].port = 0;
   if (!line) {
     disconnect_client(c);
     return;
   }
 
+  /* Client activity, so update the timer. */
+  reset_client_timer(c, 1);
+
   /* See if the policy file has changed since we last looked.  If so, try to
    * read the new version.
    */
-  if (fwatch_update(&polfw, "yaid.policy")) {
-    logmsg(0, LOG_INFO, "reload master policy file `%s'", "yaid.policy");
-    load_policy_file("yaid.policy", &policy);
+  if (fwatch_update(&polfw, policyfile)) {
+    logmsg(0, LOG_INFO, "reload master policy file `%s'", policyfile);
+    load_policy_file(policyfile, &policy);
   }
 
   /* Read the local and remote port numbers into the query structure. */
@@ -655,13 +758,13 @@ static void client_line(char *line, size_t len, void *p)
      */
     DRESET(&d);
     dstr_putf(&d, "%s/.yaid.policy", pw->pw_dir);
-    if (open_policy_file(&pf, d.buf, "user policy file", &c->q))
+    if (open_policy_file(&pf, d.buf, "user policy file", &c->q, OPF_NOENTOK))
       continue;
-    while (!read_policy_file(&pf)) {
+    while ((t = read_policy_file(&pf)) < T_ERROR) {
 
-      /* Give up after 100 lines.  If the user's policy is that complicated,
-       * something's gone very wrong.  Or there's too much commentary or
-       * something.
+      /* Give up after 100 lines or if there's an error.  If the user's
+       * policy is that complicated, something's gone very wrong.  Or there's
+       * too much commentary or something.
        */
       if (pf.lno > 100) {
        logmsg(&c->q, LOG_ERR, "%s:%d: user policy file too long",
@@ -669,6 +772,9 @@ static void client_line(char *line, size_t len, void *p)
        break;
       }
 
+      /* If this was a blank line, just go around again. */
+      if (t != T_OK) continue;
+
       /* If this isn't a match, go around for the next rule. */
       if (!match_policy(&pf.p, &c->q)) continue;
 
@@ -755,7 +861,7 @@ static void accept_client(int fd, unsigned mode, void *p)
   struct listen *l = p;
   struct client *c;
   struct sockaddr_storage ssr, ssl;
-  size_t ssz = sizeof(ssr);
+  socklen_t ssz = sizeof(ssr);
   int sk;
 
   /* Accept the new connection. */
@@ -774,7 +880,7 @@ static void accept_client(int fd, unsigned mode, void *p)
   c->q.ao = l->ao;
 
   /* Collect the local and remote addresses. */
-  l->ao->sockaddr_to_addr(&ssr, &c->q.s[R].addr);
+  l->ao->sockaddr_to_addr(&ssr, &c->raddr);
   ssz = sizeof(ssl);
   if (getsockname(sk, (struct sockaddr *)&ssl, &ssz)) {
     logmsg(0, LOG_ERR,
@@ -790,6 +896,7 @@ static void accept_client(int fd, unsigned mode, void *p)
   /* Set stuff up for reading the query and sending responses. */
   selbuf_init(&c->b, &sel, sk, client_line, c);
   selbuf_setsize(&c->b, 1024);
+  reset_client_timer(c, 0);
   c->fd = sk;
   c->px = 0;
   init_writebuf(&c->wb, sk, done_client_write, c);
@@ -854,37 +961,204 @@ static int make_listening_socket(const struct addrops *ao, int port)
   return (0);
 }
 
+/* Quit because of a fatal signal. */
+static void NORETURN quit(int sig, void *p)
+{
+  const char *signame = p;
+
+  logmsg(0, LOG_NOTICE, "shutting down on %s", signame);
+  if (pidfile) unlink(pidfile);
+  exit(0);
+}
+
+/* Answer whether the string pointed to by P consists entirely of digits. */
+static int numericp(const char *p)
+{
+  while (*p)
+    if (!isdigit((unsigned char)*p++)) return (0);
+  return (1);
+}
+
+static void usage(FILE *fp)
+{
+  pquis(fp, "Usage: $ [-Dl] [-G GROUP] [-U USER] [-P FILE] "
+       "[-c FILE] [-p PORT]\n");
+}
+
+static void version(FILE *fp)
+  { pquis(fp, "$, version " VERSION "\n"); }
+
+static void help(FILE *fp)
+{
+  version(fp); fputc('\n', fp);
+  usage(fp);
+  fputs("\n\
+Yet Another Ident Daemon.  Really, the world doesn't need such a thing.\n\
+It's just a shame none of the others do the right things.\n\
+\n\
+Options:\n\
+\n\
+  -h, --help           Show this help message.\n\
+  -v, --version                Show the version number.\n\
+  -u, --usage          Show a very short usage summary.\n\
+\n\
+  -D, --daemon         Become a daemon, running in the background.\n\
+  -G, --group=GROUP    Set group after initialization.\n\
+  -P, --pidfile=FILE   Write process id to FILE.\n\
+  -U, --user=USER      Set user after initialization.\n\
+  -c, --config=FILE    Read global policy from FILE.\n\
+  -l, --syslog         Write log messages using syslog(3).\n\
+  -p, --port=PORT      Listen for connections on this port.\n",
+       fp);
+}
+
 int main(int argc, char *argv[])
 {
   int port = 113;
+  uid_t u = -1;
+  gid_t g = -1;
+  struct passwd *pw = 0;
+  struct group *gr;
+  struct servent *s;
+  sig sigint, sigterm;
+  FILE *fp = 0;
+  int i;
+  unsigned f = 0;
+#define f_bogus 1u
+#define f_daemon 2u
   const struct addrops *ao;
   int any = 0;
 
   ego(argv[0]);
 
-  fwatch_init(&polfw, "yaid.policy");
-  init_sys();
-  if (load_policy_file("yaid.policy", &policy))
-    exit(1);
-  { int i;
-    for (i = 0; i < DA_LEN(&policy); i++)
-      print_policy(&DA(&policy)[i]);
+  /* Parse command-line options. */
+  for (;;) {
+    const struct option opts[] = {
+      { "help",                0,              0,      'h' },
+      { "version",     0,              0,      'v' },
+      { "usage",       0,              0,      'u' },
+      { "daemon",      0,              0,      'D' },
+      { "group",       OPTF_ARGREQ,    0,      'G' },
+      { "pidfile",     OPTF_ARGREQ,    0,      'P' },
+      { "user",                OPTF_ARGREQ,    0,      'U' },
+      { "config",      OPTF_ARGREQ,    0,      'c' },
+      { "syslog",      0,              0,      'l' },
+      { "port",                OPTF_ARGREQ,    0,      'p' },
+      { 0,             0,              0,      0 }
+    };
+
+    if ((i = mdwopt(argc, argv, "hvuDG:P:U:c:lp:", opts, 0, 0, 0)) < 0)
+      break;
+    switch (i) {
+      case 'h': help(stdout); exit(0);
+      case 'v': version(stdout); exit(0);
+      case 'u': usage(stdout); exit(0);
+      case 'D': f |= f_daemon; break;
+      case 'P': pidfile = optarg; break;
+      case 'c': policyfile = optarg; break;
+      case 'l': flags |= F_SYSLOG; break;
+      case 'G':
+       if (numericp(optarg))
+         g = atoi(optarg);
+       else if ((gr = getgrnam(optarg)) == 0)
+         die(1, "unknown group `%s'", optarg);
+       else
+         g = gr->gr_gid;
+       break;
+      case 'U':
+       if (numericp(optarg))
+         u = atoi(optarg);
+       else if ((pw = getpwnam(optarg)) == 0)
+         die(1, "unknown user `%s'", optarg);
+       else
+         u = pw->pw_uid;
+       break;
+      case 'p':
+       if (numericp(optarg))
+         port = atoi(optarg);
+       else if ((s = getservbyname(optarg, "tcp")) == 0)
+         die(1, "unknown service name `%s'", optarg);
+       else
+         port = ntohs(s->s_port);
+       break;
+      default: f |= f_bogus; break;
+    }
   }
+  if (optind < argc) f |= f_bogus;
+  if (f & f_bogus) { usage(stderr); exit(1); }
 
-  if ((randfd = open("/dev/urandom", O_RDONLY)) < 0) {
-    die(1, "failed to open `/dev/urandom' for reading: %s",
-       strerror(errno));
+  /* If a user has been requested, but no group, then find the user's primary
+   * group.  If the user was given by name, then we already have a password
+   * entry and should use that, in case two differently-named users have the
+   * same uid but distinct gids.
+   */
+  if (u != -1 && g == -1) {
+    if (!pw && (pw = getpwuid(u)) == 0) {
+      die(1, "failed to find password entry for user %d: "
+         "request group explicitly", u);
+    }
+    g = pw->pw_gid;
   }
 
+  /* Initialize system-specific machinery. */
+  init_sys();
+
+  /* Load the global policy rules. */
+  fwatch_init(&polfw, policyfile);
+  if (load_policy_file(policyfile, &policy))
+    exit(1);
+
+  /* Set up the I/O event system. */
   sel_init(&sel);
+
+  /* Watch for some interesting signals. */
+  sig_init(&sel);
+  sig_add(&sigint, SIGINT, quit, "SIGINT");
+  sig_add(&sigterm, SIGTERM, quit, "SIGTERM");
+
+  /* Listen for incoming connections. */
   for (ao = addroptab; ao->name; ao++)
     if (!make_listening_socket(ao, port)) any = 1;
-  if (!any)
-    die(1, "no IP protocols supported");
+  if (!any) die(1, "no IP protocols supported");
+
+  /* Open the pidfile now, in case it's somewhere we can't write. */
+  if (pidfile && (fp = fopen(pidfile, "w")) == 0) {
+    die(1, "failed to open pidfile `%s' for writing: %s",
+       pidfile, strerror(errno));
+  }
+
+  /* If we're meant to use syslog, then open the log. */
+  if (flags & F_SYSLOG)
+    openlog(QUIS, 0, LOG_DAEMON);
+
+  /* Drop privileges. */
+  if ((g != -1 && (setegid(g) || setgid(g) ||
+                  (getuid() == 0 && setgroups(1, &g)))) ||
+      (u != -1 && setuid(u)))
+    die(1, "failed to drop privileges: %s", strerror(errno));
 
-  for (;;)
-    if (sel_select(&sel)) die(1, "select failed: %s", strerror(errno));
+  /* Become a background process, if requested. */
+  if ((f & f_daemon) && daemonize())
+    die(1, "failed to become daemon: %s", strerror(errno));
+
+  /* Write the process id to the pidfile. */
+  if (fp) {
+    fprintf(fp, "%d\n", getpid());
+    fclose(fp);
+  }
+
+  /* And now we're going. */
+  flags |= F_RUNNING;
+
+  /* Read events and process them. */
+  for (;;) {
+    if (sel_select(&sel) && errno != EINTR)
+      die(1, "select failed: %s", strerror(errno));
+    reap_dead_proxies();
+    reap_dead_clients();
+  }
 
+  /* This just keeps the compiler happy. */
   return (0);
 }