+/* -*-c-*-
+ *
+ * Main daemon
+ *
+ * (c) 2012 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of Yet Another Ident Daemon (YAID).
+ *
+ * YAID is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * YAID is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with YAID; if not, write to the Free Software Foundation,
+ * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include "yaid.h"
+
+/*----- Data structures ---------------------------------------------------*/
+
+struct listen {
+ int af;
+ const char *proto;
+ sel_file f;
+};
+
+#define WRBUFSZ 1024
+struct writebuf {
+ size_t o, n;
+ sel_file wr;
+ void (*func)(int, void *);
+ void *p;
+ unsigned char buf[WRBUFSZ];
+};
+
+struct proxy {
+ struct client *c;
+ int fd;
+ conn cn;
+ selbuf b;
+ struct writebuf wb;
+ char nat[ADDRLEN];
+};
+
+struct client {
+ selbuf b;
+ int fd;
+ struct query q;
+ struct listen *l;
+ struct writebuf wb;
+ struct proxy *px;
+};
+
+/*----- Static variables --------------------------------------------------*/
+
+static sel_state sel;
+
+static policy_v policy = DA_INIT;
+static fwatch polfw;
+
+static unsigned char tokenbuf[4096];
+static size_t tokenptr = sizeof(tokenbuf);
+static int randfd;
+
+/*----- Main code ---------------------------------------------------------*/
+
+static void socket_to_sockaddr(int af, const struct socket *s,
+ struct sockaddr *sa, size_t *ssz)
+{
+ sa->sa_family = af;
+ switch (af) {
+ case AF_INET: {
+ struct sockaddr_in *sin = (struct sockaddr_in *)sa;
+ sin->sin_addr = s->addr.ipv4;
+ sin->sin_port = htons(s->port);
+ *ssz = sizeof(*sin);
+ } break;
+ case AF_INET6: {
+ struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)sa;
+ sin6->sin6_addr = s->addr.ipv6;
+ sin6->sin6_port = htons(s->port);
+ sin6->sin6_flowinfo = 0;
+ sin6->sin6_scope_id = 0;
+ *ssz = sizeof(*sin6);
+ } break;
+ default: abort();
+ }
+}
+
+static void sockaddr_to_addr(const struct sockaddr *sa, union addr *a)
+{
+ switch (sa->sa_family) {
+ case AF_INET: a->ipv4 = ((struct sockaddr_in *)sa)->sin_addr; break;
+ case AF_INET6: a->ipv6 = ((struct sockaddr_in6 *)sa)->sin6_addr; break;
+ default: abort();
+ }
+}
+
+static void dputsock(dstr *d, int af, const struct socket *s)
+{
+ char buf[ADDRLEN];
+
+ inet_ntop(af, &s->addr, buf, sizeof(buf));
+ if (!s->port || af != AF_INET6) dstr_puts(d, buf);
+ else { dstr_putc(d, '['); dstr_puts(d, buf); dstr_putc(d, ']'); }
+ if (s->port) dstr_putf(d, ":%d", s->port);
+}
+
+void logmsg(const struct query *q, int prio, const char *msg, ...)
+{
+ va_list ap;
+ dstr d = DSTR_INIT;
+
+ va_start(ap, msg);
+ if (q) {
+ dputsock(&d, q->af, &q->s[L]);
+ dstr_puts(&d, " <-> ");
+ dputsock(&d, q->af, &q->s[R]);
+ dstr_puts(&d, ": ");
+ }
+ dstr_vputf(&d, msg, &ap);
+ va_end(ap);
+ fprintf(stderr, "yaid: %s\n", d.buf);
+ dstr_destroy(&d);
+}
+
+static void write_out(int fd, unsigned mode, void *p)
+{
+ ssize_t n;
+ struct writebuf *wb = p;
+
+ if ((n = write(fd, wb->buf + wb->o, wb->n)) < 0) {
+ if (errno == EAGAIN || errno == EWOULDBLOCK) return;
+ wb->n = 0;
+ sel_rmfile(&wb->wr);
+ wb->func(errno, wb->p);
+ }
+ wb->o += n;
+ wb->n -= n;
+ if (!wb->n) {
+ wb->o = 0;
+ sel_rmfile(&wb->wr);
+ wb->func(0, wb->p);
+ }
+}
+
+static int queue_write(struct writebuf *wb, const void *p, size_t n)
+{
+ if (!n) return (0);
+ if (wb->n - wb->o + n > WRBUFSZ) return (-1);
+ if (wb->o) {
+ memmove(wb->buf, wb->buf + wb->o, wb->n);
+ wb->o = 0;
+ }
+ memcpy(wb->buf + wb->n, p, n);
+ if (!wb->n) {
+ sel_addfile(&wb->wr);
+ sel_force(&wb->wr);
+ }
+ wb->n += n;
+ return (0);
+}
+
+static void free_writebuf(struct writebuf *wb)
+ { if (wb->n) sel_rmfile(&wb->wr); }
+
+static void init_writebuf(struct writebuf *wb,
+ int fd, void (*func)(int, void *), void *p)
+{
+ sel_initfile(&sel, &wb->wr, fd, SEL_WRITE, write_out, wb);
+ wb->func = func;
+ wb->p = p;
+ wb->n = wb->o = 0;
+}
+
+static void cancel_proxy(struct proxy *px)
+{
+ if (px->fd == -1)
+ conn_kill(&px->cn);
+ else {
+ close(px->fd);
+ selbuf_destroy(&px->b);
+ free_writebuf(&px->wb);
+ }
+ selbuf_enable(&px->c->b);
+ px->c->px = 0;
+ xfree(px);
+}
+
+static void disconnect_client(struct client *c)
+{
+ close(c->fd);
+ selbuf_destroy(&c->b);
+ free_writebuf(&c->wb);
+ if (c->px) cancel_proxy(c->px);
+ xfree(c);
+}
+
+static void done_client_write(int err, void *p)
+{
+ struct client *c = p;
+
+ if (!err)
+ selbuf_enable(&c->b);
+ else {
+ logmsg(&c->q, LOG_ERR, "failed to send reply: %s", strerror(err));
+ disconnect_client(c);
+ }
+}
+
+static void write_to_client(struct client *c, const char *fmt, ...)
+{
+ va_list ap;
+ char buf[WRBUFSZ];
+ ssize_t n;
+
+ va_start(ap, fmt);
+ n = vsnprintf(buf, sizeof(buf), fmt, ap);
+ if (n < 0) {
+ logmsg(&c->q, LOG_ERR, "failed to format output: %s", strerror(errno));
+ disconnect_client(c);
+ return;
+ } else if (n > sizeof(buf)) {
+ logmsg(&c->q, LOG_ERR, "output too long for client send buffer");
+ disconnect_client(c);
+ return;
+ }
+
+ selbuf_disable(&c->b);
+ if (queue_write(&c->wb, buf, n)) {
+ logmsg(&c->q, LOG_ERR, "write buffer overflow");
+ disconnect_client(c);
+ }
+}
+
+static void reply(struct client *c, const char *ty, const char *msg)
+{
+ write_to_client(c, "%u,%u:%s:%s\r\n",
+ c->q.s[L].port, c->q.s[R].port, ty, msg);
+}
+
+static void reply_error(struct client *c, unsigned err)
+{
+ assert(err < E_LIMIT);
+ reply(c, "ERROR", errtok[err]);
+}
+
+static void skipws(const char **pp)
+ { while (isspace((unsigned char )**pp)) (*pp)++; }
+
+static int idtoken(const char **pp, char *q, size_t n)
+{
+ const char *p = *pp;
+
+ skipws(&p);
+ n--;
+ for (;;) {
+ if (*p == ':' || *p <= 32 || *p >= 127) break;
+ if (!n) return (-1);
+ *q++ = *p++;
+ n--;
+ }
+ *q++ = 0;
+ *pp = p;
+ return (0);
+}
+
+static int unum(const char **pp, unsigned *ii, unsigned min, unsigned max)
+{
+ char *q;
+ unsigned long i;
+ int e;
+
+ skipws(pp);
+ if (!isdigit((unsigned char)**pp)) return (-1);
+ e = errno; errno = 0;
+ i = strtoul(*pp, &q, 10);
+ if (errno) return (-1);
+ *pp = q;
+ errno = e;
+ if (i < min || i > max) return (-1);
+ *ii = i;
+ return (0);
+}
+
+static void proxy_line(char *line, size_t sz, void *p)
+{
+ struct proxy *px = p;
+ char buf[1024];
+ const char *q = line;
+ unsigned lp, rp;
+
+ while (sz && isspace((unsigned char)line[sz - 1])) sz--;
+ printf("received proxy line from %s: %s\n", px->nat, line);
+
+ if (unum(&q, &lp, 1, 65535)) goto syntax;
+ skipws(&q); if (*q != ',') goto syntax; q++;
+ if (unum(&q, &rp, 1, 65535)) goto syntax;
+ skipws(&q); if (*q != ':') goto syntax; q++;
+ if (lp != px->c->q.u.nat.port || rp != px->c->q.s[R].port) goto syntax;
+ if (idtoken(&q, buf, sizeof(buf))) goto syntax;
+ skipws(&q); if (*q != ':') goto syntax; q++;
+ if (strcmp(buf, "ERROR") == 0) {
+ skipws(&q);
+ logmsg(&px->c->q, LOG_ERR, "proxy error from %s: %s", px->nat, q);
+ reply(px->c, "ERROR", q);
+ } else if (strcmp(buf, "USERID") == 0) {
+ if (idtoken(&q, buf, sizeof(buf))) goto syntax;
+ skipws(&q); if (*q != ':') goto syntax; q++;
+ skipws(&q);
+ logmsg(&px->c->q, LOG_ERR, "user `%s'; proxy = %s, os = %s",
+ q, px->nat, buf);
+ write_to_client(px->c, "%u,%u:USERID:%s:%s\r\n",
+ px->c->q.s[L].port, px->c->q.s[R].port, buf, q);
+ } else
+ goto syntax;
+ goto done;
+
+syntax:
+ logmsg(&px->c->q, LOG_ERR, "failed to parse response from %s", px->nat);
+ reply_error(px->c, E_UNKNOWN);
+done:
+ cancel_proxy(px);
+}
+
+static void done_proxy_write(int err, void *p)
+{
+ struct proxy *px = p;
+
+ if (err) {
+ logmsg(&px->c->q, LOG_ERR, "failed to proxy query to %s: %s",
+ px->nat, strerror(errno));
+ reply_error(px->c, E_UNKNOWN);
+ cancel_proxy(px);
+ return;
+ }
+ selbuf_enable(&px->b);
+}
+
+static void proxy_connected(int fd, void *p)
+{
+ struct proxy *px = p;
+ char buf[16];
+ int n;
+
+ if (fd < 0) {
+ logmsg(&px->c->q, LOG_ERR,
+ "failed to make %s proxy connection to %s: %s",
+ px->c->l->proto, px->nat, strerror(errno));
+ reply_error(px->c, E_UNKNOWN);
+ cancel_proxy(px);
+ return;
+ }
+
+ px->fd = fd;
+ selbuf_init(&px->b, &sel, fd, proxy_line, px);
+ selbuf_setsize(&px->b, 1024);
+ selbuf_disable(&px->b);
+ init_writebuf(&px->wb, fd, done_proxy_write, px);
+
+ n = sprintf(buf, "%u,%u\r\n", px->c->q.u.nat.port, px->c->q.s[R].port);
+ queue_write(&px->wb, buf, n);
+}
+
+static void proxy_query(struct client *c)
+{
+ struct socket s;
+ struct sockaddr_storage ss;
+ size_t ssz;
+ struct proxy *px;
+ int o;
+ int fd;
+
+ px = xmalloc(sizeof(*px));
+ inet_ntop(c->q.af, &c->q.u.nat.addr, px->nat, sizeof(px->nat));
+
+ if ((fd = socket(c->q.af, SOCK_STREAM, 0)) < 0) {
+ logmsg(&c->q, LOG_ERR, "failed to make %s socket for proxy: %s",
+ c->l->proto, strerror(errno));
+ goto err_0;
+ }
+
+ if ((o = fcntl(fd, F_GETFL)) < 0 ||
+ fcntl(fd, F_SETFL, o | O_NONBLOCK)) {
+ logmsg(&c->q, LOG_ERR, "failed to set %s proxy socket nonblocking: %s",
+ c->l->proto, strerror(errno));
+ goto err_1;
+ }
+
+ s = c->q.u.nat;
+ s.port = 113;
+ socket_to_sockaddr(c->q.af, &s, (struct sockaddr *)&ss, &ssz);
+ selbuf_disable(&c->b);
+ if (conn_init(&px->cn, &sel, fd, (struct sockaddr *)&ss, ssz,
+ proxy_connected, px)) {
+ logmsg(&c->q, LOG_ERR, "failed to make %s proxy connection to %s: %s",
+ c->l->proto, px->nat, strerror(errno));
+ goto err_2;
+ }
+
+ c->px = px; px->c = c;
+ px->fd = -1;
+ return;
+
+err_2:
+ selbuf_enable(&c->b);
+err_1:
+ close(px->fd);
+err_0:
+ xfree(px);
+ reply_error(c, E_UNKNOWN);
+}
+
+static const struct policy default_policy = POLICY_INIT(A_NAME);
+
+static void user_token(char *p)
+{
+ static const char tokmap[64] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.-";
+ unsigned a = 0;
+ unsigned b = 0;
+ int i;
+#define TOKENSZ 8
+
+ if (tokenptr + TOKENSZ >= sizeof(tokenbuf)) {
+ if (read(randfd, tokenbuf, sizeof(tokenbuf)) < sizeof(tokenbuf))
+ die(1, "unexpected short read or error from `/dev/urandom'");
+ tokenptr = 0;
+ }
+
+ for (i = 0; i < TOKENSZ; i++) {
+ a = (a << 8) | tokenbuf[tokenptr++]; b += 8;
+ while (b >= 6) {
+ b -= 6;
+ *p++ = tokmap[(a >> b) & 0x3f];
+ }
+ }
+ if (b)
+ *p++ = tokmap[(a << (6 - b)) & 0x3f];
+ *p++ = 0;
+}
+
+static void client_line(char *line, size_t len, void *p)
+{
+ struct client *c = p;
+ const char *q;
+ struct passwd *pw = 0;
+ const struct policy *pol;
+ dstr d = DSTR_INIT;
+ struct policy upol = POLICY_INIT(A_LIMIT);
+ struct policy_file pf;
+ char buf[16];
+ int i;
+
+ c->q.s[L].port = c->q.s[R].port = 0;
+ if (!line) {
+ disconnect_client(c);
+ return;
+ }
+
+ if (fwatch_update(&polfw, "yaid.policy")) {
+ logmsg(0, LOG_INFO, "reload master policy file `%s'", "yaid.policy");
+ load_policy_file("yaid.policy", &policy);
+ }
+
+ q = line;
+ if (unum(&q, &c->q.s[L].port, 1, 65535)) goto bad;
+ skipws(&q); if (*q != ',') goto bad; q++;
+ if (unum(&q, &c->q.s[R].port, 1, 65535)) goto bad;
+ skipws(&q); if (*q) goto bad;
+
+ identify(&c->q);
+ switch (c->q.resp) {
+ case R_UID:
+ if ((pw = getpwuid(c->q.u.uid)) == 0) {
+ logmsg(&c->q, LOG_ERR, "no passwd entry for user %d", c->q.u.uid);
+ reply_error(c, E_NOUSER);
+ return;
+ }
+ break;
+ case R_NAT:
+ proxy_query(c);
+ return;
+ case R_ERROR:
+ /* Should already be logged. */
+ reply_error(c, c->q.u.error);
+ return;
+ default:
+ abort();
+ }
+
+ for (i = 0; i < DA_LEN(&policy); i++) {
+ pol = &DA(&policy)[i];
+ if (!match_policy(pol, &c->q)) continue;
+ if (pol->act.act != A_USER)
+ goto match;
+ DRESET(&d);
+ dstr_putf(&d, "%s/.yaid.policy", pw->pw_dir);
+ if (open_policy_file(&pf, d.buf, "user policy file", &c->q))
+ continue;
+ while (!read_policy_file(&pf)) {
+ if (pf.lno > 100) {
+ logmsg(&c->q, LOG_ERR, "%s:%d: user policy file too long",
+ pf.name, pf.lno);
+ break;
+ }
+ if (!match_policy(&pf.p, &c->q)) continue;
+ if (!(pol->act.u.user & (1 << pf.p.act.act))) {
+ logmsg(&c->q, LOG_ERR,
+ "%s:%d: user action forbidden by global policy",
+ pf.name, pf.lno);
+ continue;
+ }
+ upol = pf.p; pol = &upol;
+ init_policy(&pf.p);
+ close_policy_file(&pf);
+ goto match;
+ }
+ close_policy_file(&pf);
+ }
+ pol = &default_policy;
+
+match:
+ DDESTROY(&d);
+ switch (pol->act.act) {
+ case A_NAME:
+ logmsg(&c->q, LOG_INFO, "user `%s' (%d)", pw->pw_name, c->q.u.uid);
+ reply(c, "USERID:UNIX", pw->pw_name);
+ break;
+ case A_TOKEN:
+ user_token(buf);
+ logmsg(&c->q, LOG_INFO, "user `%s' (%d); token = %s",
+ pw->pw_name, c->q.u.uid, buf);
+ reply(c, "USERID:OTHER", buf);
+ break;
+ case A_DENY:
+ logmsg(&c->q, LOG_INFO, "user `%s' (%d); denying",
+ pw->pw_name, c->q.u.uid);
+ break;
+ case A_HIDE:
+ logmsg(&c->q, LOG_INFO, "user `%s' (%d); hiding",
+ pw->pw_name, c->q.u.uid);
+ reply_error(c, E_HIDDEN);
+ break;
+ case A_LIE:
+ logmsg(&c->q, LOG_INFO, "user `%s' (%d); lie = `%s'",
+ pw->pw_name, c->q.u.uid, pol->act.u.lie);
+ reply(c, "USERID:UNIX", pol->act.u.lie);
+ break;
+ default:
+ abort();
+ }
+
+ free_policy(&upol);
+ return;
+
+bad:
+ logmsg(&c->q, LOG_ERR, "failed to parse query from client");
+ disconnect_client(c);
+}
+
+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);
+ int sk;
+
+ if ((sk = accept(fd, (struct sockaddr *)&ssr, &ssz)) < 0) {
+ if (errno != EAGAIN && errno == EWOULDBLOCK) {
+ logmsg(0, LOG_ERR, "failed to accept incoming %s connection: %s",
+ l->proto, strerror(errno));
+ }
+ return;
+ }
+
+ c = xmalloc(sizeof(*c));
+ c->l = l;
+ c->q.af = l->af;
+ sockaddr_to_addr((struct sockaddr *)&ssr, &c->q.s[R].addr);
+ ssz = sizeof(ssl);
+ if (getsockname(sk, (struct sockaddr *)&ssl, &ssz)) {
+ logmsg(0, LOG_ERR,
+ "failed to read local address for incoming %s connection: %s",
+ l->proto, strerror(errno));
+ close(sk);
+ xfree(c);
+ return;
+ }
+ sockaddr_to_addr((struct sockaddr *)&ssl, &c->q.s[L].addr);
+ c->q.s[L].port = c->q.s[R].port = 0;
+
+ /* logmsg(&c->q, LOG_INFO, "accepted %s connection", l->proto); */
+
+ selbuf_init(&c->b, &sel, sk, client_line, c);
+ selbuf_setsize(&c->b, 1024);
+ c->fd = sk;
+ c->px = 0;
+ init_writebuf(&c->wb, sk, done_client_write, c);
+}
+
+static int make_listening_socket(int af, int port, const char *proto)
+{
+ int fd;
+ int o;
+ struct sockaddr_storage ss;
+ struct listen *l;
+ size_t ssz;
+
+ if ((fd = socket(af, SOCK_STREAM, 0)) < 0) {
+ die(1, "failed to create %s listening socket: %s",
+ proto, strerror(errno));
+ }
+ o = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &o, sizeof(o));
+ ss.ss_family = af;
+ switch (af) {
+ case AF_INET: {
+ struct sockaddr_in *sin = (struct sockaddr_in *)&ss;
+ sin->sin_addr.s_addr = INADDR_ANY;
+ sin->sin_port = htons(port);
+ ssz = sizeof(*sin);
+ } break;
+ case AF_INET6: {
+ struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)&ss;
+ o = 1; setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &o, sizeof(o));
+ sin6->sin6_family = AF_INET6;
+ sin6->sin6_addr = in6addr_any;
+ sin6->sin6_scope_id = 0;
+ sin6->sin6_flowinfo = 0;
+ ssz = sizeof(*sin6);
+ } break;
+ default:
+ abort();
+ }
+ if (bind(fd, (struct sockaddr *)&ss, ssz))
+ die(1, "failed to bind %s listening socket: %s", proto, strerror(errno));
+ if ((o = fcntl(fd, F_GETFL)) < 0 ||
+ fcntl(fd, F_SETFL, o | O_NONBLOCK)) {
+ die(1, "failed to set %s listening socket nonblocking: %s",
+ proto, strerror(errno));
+ }
+ if (listen(fd, 5))
+ die(1, "failed to listen for %s: %s", proto, strerror(errno));
+
+ l = xmalloc(sizeof(*l));
+ l->af = af;
+ l->proto = proto;
+ sel_initfile(&sel, &l->f, fd, SEL_READ, accept_client, l);
+ sel_addfile(&l->f);
+
+ return (fd);
+}
+
+int main(int argc, char *argv[])
+{
+ int port = 113;
+ char buf[ADDRLEN];
+ union addr a;
+
+ ego(argv[0]);
+
+ fwatch_init(&polfw, "yaid.policy");
+ if (load_policy_file("yaid.policy", &policy))
+ exit(1);
+ { int i;
+ for (i = 0; i < DA_LEN(&policy); i++)
+ print_policy(&DA(&policy)[i]);
+ }
+
+ if ((randfd = open("/dev/urandom", O_RDONLY)) < 0) {
+ die(1, "failed to open `/dev/urandom' for reading: %s",
+ strerror(errno));
+ }
+
+ if (get_default_gw(AF_INET, &a))
+ printf("ipv4 gw = %s\n", inet_ntop(AF_INET, &a, buf, sizeof(buf)));
+ if (get_default_gw(AF_INET6, &a))
+ printf("ipv6 gw = %s\n", inet_ntop(AF_INET6, &a, buf, sizeof(buf)));
+
+ sel_init(&sel);
+ make_listening_socket(AF_INET, port, "IPv4");
+ make_listening_socket(AF_INET6, port, "IPv6");
+
+ for (;;)
+ if (sel_select(&sel)) die(1, "select failed: %s", strerror(errno));
+
+ return (0);
+}
+
+/*----- That's all, folks -------------------------------------------------*/