X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/yaid/blobdiff_plain/c3794524deb80d18c5b3be72a7572b34fad409a1..dd5e35da5f74e9ed3bbfb639c2bab927a12c634f:/yaid.c diff --git a/yaid.c b/yaid.c index cde2069..d0d917a 100644 --- 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); }