/*
 * userv service (or standalone program) for per-user IP subranges.
 *
 * This is the service program, which is invoked as root from userv (or may
 * be invoked firectly).
 *
 * Its arguments are supposed to be, in order, as follows:
 *
 *  The first two arguments are usually supplied by the userv
 *  configuration.  See the file `ipif/ipif' in the source tree, which
 *  is installed in /etc/userv/services.d/ipif by `make install':
 *
 *  <config>
 *
 *      Specifies address ranges and gids which own them.  The default
 *      configuration supplies /etc/userv/ipif-networks, which is then read
 *      for a list of entries, one per line.
 *
 *  --
 *      Serves to separate the user-supplied and therefore untrusted
 *      arguments from the trusted first argument.
 *
 *  The remaining arguments are supplied by the (untrusted) caller:
 *
 *  <local-addr>,<peer-addr>,<mtu>[,[<proto>][,[<ifnamepat>]]]
 *
 *      As for slattach.  The only supported protocol is slip.
 *      Alternatively, set to `debug' to print debugging info and
 *      exit.  <local-addr> is address of the interface to be created
 *      on the local system; <peer-addr> is the address of the
 *      point-to-point peer.  They must be actual addresses (not
 *      hostnames).
 *
 *  <prefix>/<mask>,<prefix>/<mask>,...
 *
 *      List of additional routes to add for this interface.  routes will
 *      be set up on the local system arranging for packets for those
 *      networks to be sent via the created interface.  <prefix> must be an
 *      IPv4 address, and mask must be an integer (dotted-quad masks are
 *      not supported).  If no additional routes are to be set up, use `-'
 *      or supply an empty argument.
 *
 * Each <config> item - whether a line in a file such as
 * /etc/userv/ipif-networks, or the single trusted argument supplied
 * on the service program command line - is one of:
 *
 *   /<config-file-name>
 *   ./<config-file-name>
 *   ../<config-file-name>
 *
 *      Reads a file which contains lines which are each <config>
 *      items.
 *
 *   <gid>,[=][-|+]<prefix>/<len>(-|+<prefix>/<len>...)[,<junk>]
 *
 *      Indicates that <gid> may allocate addresses in the relevant address
 *      range (<junk> is ignored).  <gid> must be numeric.  To specify a
 *      single host address, you must specify a mask of /32.  If `=' is
 *      specified then the specific subrange is only allowed for the local
 *      endpoint address, but not for remote addresses.
 *
 *      More than one range may be given, with each range prefixed
 *      by + or -.  In this case each address range in the rule will
 *      scanned in order, and the first range in the rule that matches
 *      any desired rule will count: if that first matching range is
 *      prefixed by `+' (or nothing) then the rule applies, if it
 *      is prefixed by `-' (or nothing matches), the rule does not.
 *
 *   *
 *      Means that anything is to be permitted.  This should not appear in
 *      /etc/userv/ipif-networks, as that would permit any user on the
 *      system to create any interfaces with any addresses and routes
 *      attached.  It is provided so that root can usefully invoke the ipif
 *      service program directly (not via userv), without needing to set up
 *      permissions in /etc/userv/ipif-networks.
 *
 *      Only `*' permits interface name patterns other than the default
 *      value of `userv%d'.
 *
 *   #...
 *
 *      Comment.  Blank lines are also ignored.
 *
 *   NB: Permission is granted if _any_ config entry matches the request.
 *
 * The service program should be run from userv with no-disconnect-hup.
 */
/*
 * This file is part of ipif, part of userv-utils
 *
 * Copyright 1996-2013 Ian Jackson <ijackson@chiark.greenend.org.uk>
 * Copyright 1998 David Damerell <damerell@chiark.greenend.org.uk>
 * Copyright 1999,2003
 *    Chancellor Masters and Scholars of the University of Cambridge
 * Copyright 2010 Tony Finch <fanf@dotat.at>
 *
 * This 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 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 userv-utils; if not, see http://www.gnu.org/licenses/.
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <errno.h>
#include <stdarg.h>
#include <ctype.h>
#include <limits.h>
#include <signal.h>
#include <unistd.h>
#include <stdint.h>
#include <poll.h>
#include <stddef.h>

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>

#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>

#include <sys/stat.h>
#include <fcntl.h>

#include <linux/if.h>
#include <linux/if_tun.h>

#define NARGS 4
#define MAXEXROUTES 50
#define ATXTLEN 16

static const unsigned long gidmaxval= (unsigned long)((gid_t)-2);
static const char *const protos_ok[]= { "slip", 0 };
static const char default_ifnamepat[]= "userv%d";

static const char *configstr, *proto;
static unsigned long localaddr, peeraddr, mtu;
static int localpming, peerpming;
static int localallow, peerallow, ifnameallow, allallow;
static char *ifnamepat;
static int nexroutes;
static struct exroute {
  unsigned long prefix, mask;
  int allow, pming;
  char prefixtxt[ATXTLEN], masktxt[ATXTLEN];
} exroutes[MAXEXROUTES];

static char localtxt[ATXTLEN];
static char peertxt[ATXTLEN];

static struct pplace {
  struct pplace *parent;
  const char *filename;
  int lineno;
} *cpplace;


static int tunfd;
static char *ifname;


static void terminate(int estatus) {
  exit(estatus);
}


static void fatal(const char *fmt, ...)
     __attribute__((format(printf,1,2)));
static void fatal(const char *fmt, ...) {
  va_list al;
  va_start(al,fmt);

  fputs("userv-ipif service: fatal error: ",stderr);
  vfprintf(stderr, fmt, al);
  putc('\n',stderr);
  terminate(8);
}
  
static void sysfatal(const char *fmt, ...)
     __attribute__((format(printf,1,2)));
static void sysfatal(const char *fmt, ...) {
  va_list al;
  int e;

  e= errno;
  va_start(al,fmt);

  fputs("userv-ipif service: fatal system error: ",stderr);
  vfprintf(stderr, fmt, al);
  fprintf(stderr,": %s\n", strerror(e));
  terminate(12);
}


static void badusage(const char *fmt, ...)
     __attribute__((format(printf,1,2)));
static void badusage(const char *fmt, ...) {
  va_list al;
  struct pplace *cpp;
  
  if (cpplace) {
    fprintf(stderr,
	    "userv-ipif service: %s:%d: ",
	    cpplace->filename, cpplace->lineno);
  } else {
    fputs("userv-ipif service: invalid usage: ",stderr);
  }
  va_start(al,fmt);
  vfprintf(stderr, fmt, al);
  putc('\n',stderr);

  if (cpplace) {
    for (cpp=cpplace->parent; cpp; cpp=cpp->parent) {
      fprintf(stderr,
	      "userv-ipif service: %s:%d: ... in file included from here\n",
	      cpp->filename, cpp->lineno);
    }
  }
  terminate(16);
}

static char *ip2txt(unsigned long addr, char *buf) {
  sprintf(buf, "%lu.%lu.%lu.%lu",
	  (addr>>24) & 0x0ff,
	  (addr>>16) & 0x0ff,
	  (addr>>8) & 0x0ff,
	  (addr) & 0x0ff);
  return buf;
}

static unsigned long eat_number(const char **argp, const char *what,
				unsigned long min, unsigned long max,
				const char *endchars, int *endchar_r) {
  /* If !endchars then the endchar must be a nul, otherwise it may be
   * a nul (resulting in *argp set to 0) or something else (*argp set
   * to point to after delim, *endchar_r set to delim).
   * *endchar_r may be 0.
   */
  unsigned long rv;
  char *ep;
  int endchar;

  if (!*argp) { badusage("missing number %s",what); }
  rv= strtoul(*argp,&ep,0);
  if ((endchar= *ep)) {
    if (!endchars) badusage("junk after number %s",what);
    if (!strchr(endchars,endchar))
      badusage("invalid character or delimiter `%c' in or after number, %s:"
	       " expected %s (or none?)", endchar,what,endchars);
    *argp= ep+1;
  } else {
    *argp= 0;
  }
  if (endchar_r) *endchar_r= endchar;
  if (rv < min || rv > max) badusage("number %s value %lu out of range %lu..%lu",
				     what, rv, min, max);
  return rv;
}

static int addrnet_overlap(unsigned long p1, unsigned long m1,
			   unsigned long p2, unsigned long m2) {
  unsigned long mask;

  mask= m1&m2;
  return (p1 & mask) == (p2 & mask);
}

static void addrnet_mustdiffer(const char *w1, unsigned long p1, unsigned long m1,
			       const char *w2, unsigned long p2, unsigned long m2) {
  if (!addrnet_overlap(p1,m1,p2,m2)) return;
  badusage("%s %08lx/%08lx overlaps/clashes with %s %08lx/%08lx",
	   w1,p1,m1, w2,p2,m2);
}
  
static unsigned long eat_addr(const char **argp, const char *what,
			      const char *endchars, int *endchar_r) {
  char whatbuf[100];
  unsigned long rv;
  int i;

  for (rv=0, i=0;
       i<4;
       i++) {
    rv <<= 8;
    sprintf(whatbuf,"%s byte #%d",what,i);
    rv |= eat_number(argp,whatbuf, 0,255, i<3 ? "." : endchars, endchar_r);
  }

  return rv;
}

static void eat_prefixmask(const char **argp, const char *what,
			   const char *endchars, int *endchar_r,
			   unsigned long *prefix_r, unsigned long *mask_r, int *len_r) {
  /* mask_r and len_r may be 0 */
  char whatbuf[100];
  int len;
  unsigned long prefix, mask;

  prefix= eat_addr(argp,what, "/",0);
  sprintf(whatbuf,"%s length",what);
  len= eat_number(argp,whatbuf, 0,32, endchars,endchar_r);

  mask= len ? (~0UL << (32-len)) : 0UL;
  if (prefix & ~mask) badusage("%s prefix %08lx not fully contained in mask %08lx",
			       what,prefix,mask);
  *prefix_r= prefix;
  if (mask_r) *mask_r= mask;
  if (len_r) *len_r= len;
}

static char *eat_optionalstr(const char **argp,
			     const char *what,
			     const char *def) {
  ptrdiff_t len;
  const char *start= *argp;
  if (!start) {
    len = 0;
  } else {
    const char *comma= strchr(start, ',');
    if (comma) {
      len= comma - start;
      *argp= comma + 1;
    } else {
      len= strlen(start);
      *argp= 0;
    }
  }
  if (!len) {
    start= def;
    len= strlen(def);
  }
  char *r = malloc(len+1);
  if (!r) sysfatal("malloc for command line string");
  memcpy(r,start,len);
  r[len]= 0;
  return r;
}

static int addrnet_isin(unsigned long prefix, unsigned long mask,
			unsigned long mprefix, unsigned long mmask) {
  return  !(~mask & mmask)  &&  (prefix & mmask) == mprefix;
}

/* Totally hideous algorithm for parsing the config file lines.
 * For each config file line, we first see if its gid applies.  If not
 * we skip it.  Otherwise, we do
 *  permit_begin
 *     which sets <foo>pming to 1
 * for each range.  <foo>pming may be 0 if we've determined that
 * this line does not apply to <foo>.
 *  permit_range
 *     which calls permit_range_thing for each <foo>
 *        which checks to see if <foo> is inside the relevant
 *        range (for +) or overlaps it (for -) and updates
 *        <foo>allow and <foo>pming.
 */

static void permit_begin(void) {
  int i;
  
  localpming= peerpming= 1;
  for (i=0; i<nexroutes; i++) exroutes[i].pming= 1;
}

static void permit_range_thing(unsigned long tprefix, unsigned long tmask,
			       const char *what, int *tallow, int *tpming,
			       unsigned long pprefix, unsigned long pmask,
			       int plus, int *any) {
  if (plus) {
    if (!addrnet_isin(tprefix,tmask, pprefix,pmask)) return;
    if (*tpming) *tallow= 1;
  } else {
    if (!addrnet_overlap(tprefix,tmask, pprefix,pmask)) return;
    *tpming= 0;
  }
  if (!proto) printf(" %c%s", plus?'+':'-', what);
  *any= 1;
}

static void permit_range(unsigned long prefix, unsigned long mask,
			 int plus, int localonly) {
  int i, any;
  char idbuf[40];
  
  assert(!(prefix & ~mask));
  any= 0;

  permit_range_thing(localaddr,~0UL,"local", &localallow,&localpming,
		     prefix,mask, plus,&any);

  if (!localonly) {
    permit_range_thing(peeraddr,~0UL, "peer-addr", &peerallow,&peerpming,
		       prefix,mask, plus,&any);
    for (i=0; i<nexroutes; i++) {
      sprintf(idbuf,"route#%d",i);
      permit_range_thing(exroutes[i].prefix,exroutes[i].mask, idbuf,
			 &exroutes[i].allow,&exroutes[i].pming,
			 prefix,mask, plus,&any);
    }
  }
  if (!proto)
    if (!any) fputs(" nothing",stdout);
}

static void pconfig(const char *configstr, int truncated);

static void pfile(const char *filename) {
  FILE *file;
  char buf[PATH_MAX];
  int l, truncated, c;
  struct pplace npp, *cpp;

  for (cpp=cpplace; cpp; cpp=cpp->parent) {
    if (!strcmp(cpp->filename,filename))
      badusage("recursive configuration file `%s'",filename);
  }

  file= fopen(filename,"r");
  if (!file)
    badusage("cannot open configuration file `%s': %s", filename, strerror(errno));

  if (!proto) printf("config file `%s':\n",filename);

  npp.parent= cpplace;
  npp.filename= filename;
  npp.lineno= 0;
  cpplace= &npp;

  while (fgets(buf, sizeof(buf), file)) {
    npp.lineno++;
    l= strlen(buf);
    if (!l) continue;

    truncated= (buf[l-1] != '\n');
    while (l>0 && isspace((unsigned char) buf[l-1])) l--;
    if (!l) continue;
    buf[l]= 0;

    if (truncated) {
      while ((c= getc(file)) != EOF && c != '\n');
      if (c == EOF) break;
    }

    pconfig(buf,truncated);
  }
  if (ferror(file))
    badusage("failed while reading configuration file: %s", strerror(errno));

  cpplace= npp.parent;
  fclose(file);
}

static void pconfig(const char *configstr, int truncated) {
  unsigned long fgid, tgid, pprefix, pmask;
  int plen, localonly, plus, rangeix, delim;
  char ptxt[ATXTLEN];
  char whattxt[100];
  const char *gidlist;
  
  switch (configstr[0]) {
  case '*':
    permit_begin();
    permit_range(0UL,0UL,1,0);
    ifnameallow= 1;
    return;
    
  case '#':
    return;
    
  case '/': case '.':
    if (truncated) badusage("filename too long (`%.100s...')",configstr);
    pfile(configstr);
    return;
    
  default:
    if (!isdigit((unsigned char)configstr[0]))
      badusage("unknown configuration directive");
    
    fgid= eat_number(&configstr,"gid", 0,gidmaxval, ",",0);

    if (!proto) printf(" %5lu", fgid);

    gidlist= getenv("USERV_GID");
    if (!gidlist) fatal("USERV_GID not set");
    for (;;) {
      if (!gidlist) {
	if (!proto) printf(" no matching gid\n");
	return;
      }
      tgid= eat_number(&gidlist,"userv-gid", 0,gidmaxval, " ",0);
      if (tgid == fgid) break;
    }

    if (configstr[0] == '=') {
      localonly= 1;
      configstr++;
    } else {
      localonly= 0;
    }

    permit_begin();

    rangeix= 0;
    plus= 1;
    switch (configstr[0]) {
    case '-': plus= 0;     /* fall through */
    case '+': configstr++;
    default:;
    }

    for (;;) {
      sprintf(whattxt, "%s-prefix#%d",
	      plus ? "permitted" : "notpermitted",
	      rangeix);
      eat_prefixmask(&configstr,whattxt, ",+-",&delim,
		     &pprefix,&pmask,&plen);
      if (!configstr && truncated)
	badusage("gid,prefix/len,... spec too long");

      if (!proto)
	printf("  %c%s/%d:", plus?'+':'-',ip2txt(pprefix,ptxt), plen);

      permit_range(pprefix,pmask,plus,localonly);
      if (delim==',') break;

      plus= delim=='-' ? 0 : 1;
      rangeix++;
    }

    putchar('\n');
    return;
  }
}

static void checkallow(int allow, const char *what,
		       const char *prefixtxt, const char *masktxt) {
  if (allow) return;
  fprintf(stderr,"userv-ipif service: access denied for %s, %s/%s\n",
	  what, prefixtxt, masktxt);
  allallow= 0;
}

static void parseargs(int argc, const char *const *argv) {
  unsigned long routeaddr, routemask;
  const char *carg;
  const char *const *cprotop;
  int i;
  char erwhatbuf[100], erwhatbuf2[100];
  
  if (argc < NARGS+1) { badusage("too few arguments"); }
  if (argc > NARGS+1) { badusage("too many arguments"); }

  configstr= *++argv;
  
  carg= *++argv;
  if (strcmp(carg,"--")) badusage("separator argument `--' not found, got `%s'",carg);

  carg= *++argv;
  localaddr= eat_addr(&carg,"local-addr", ",",0);
  peeraddr= eat_addr(&carg,"peer-addr", ",",0);
  mtu= eat_number(&carg,"mtu", 576,65536, ",",0);
  localallow= peerallow= 0;

  char *protostr= eat_optionalstr(&carg,"protocol","slip");
  if (!strcmp(protostr,"debug")) {
    proto= 0;
  } else {
    for (cprotop= protos_ok;
	 (proto= *cprotop) && strcmp(proto,protostr);
	 cprotop++);
    if (!proto) fatal("invalid protocol");
  }

  ifnamepat= eat_optionalstr(&carg,"ifname pattern",default_ifnamepat);
  
  addrnet_mustdiffer("local-addr",localaddr,~0UL, "peer-addr",peeraddr,~0UL);
  
  carg= *++argv;
  if (strcmp(carg,"-")) {
    for (nexroutes=0;
	 carg && *carg;
	 nexroutes++) {
      if (nexroutes == MAXEXROUTES)
	fatal("too many extra routes (only %d allowed)",MAXEXROUTES);
      sprintf(erwhatbuf,"route#%d",nexroutes);
    
      eat_prefixmask(&carg,erwhatbuf, ",",0, &routeaddr,&routemask,0);
      if (routemask == ~0UL) {
	addrnet_mustdiffer(erwhatbuf,routeaddr,routemask, "local-addr",localaddr,~0UL);
	addrnet_mustdiffer(erwhatbuf,routeaddr,routemask, "peer-addr",peeraddr,~0UL);
      }
      for (i=0; i<nexroutes; i++) {
	sprintf(erwhatbuf2,"route#%d",i);
	addrnet_mustdiffer(erwhatbuf,routeaddr,routemask,
			   erwhatbuf2,exroutes[i].prefix,exroutes[i].mask);
      }
      exroutes[nexroutes].prefix= routeaddr;
      exroutes[nexroutes].mask= routemask;
      exroutes[nexroutes].allow= 0;
      ip2txt(routeaddr,exroutes[nexroutes].prefixtxt);
      ip2txt(routemask,exroutes[nexroutes].masktxt);
    }
  }

  ip2txt(localaddr,localtxt);
  ip2txt(peeraddr,peertxt);
}

static void checkpermit(void) {
  int i;
  char erwhatbuf[100];
  
  allallow= 1;
  checkallow(localallow,"local-addr", localtxt,"32");
  checkallow(peerallow,"peer-addr", peertxt,"32");
  for (i=0; i<nexroutes; i++) {
    sprintf(erwhatbuf, "route#%d", i);
    checkallow(exroutes[i].allow, erwhatbuf, exroutes[i].prefixtxt, exroutes[i].masktxt);
  }
  if (!strcmp(ifnamepat,default_ifnamepat))
    ifnameallow= 1;
  if (!ifnameallow) {
    fprintf(stderr,
	    "userv-ipif service: access denied for interface name %s\n",
	    ifnamepat);
    allallow= 0;
  }
  if (!allallow) fatal("access denied");
}

static void dumpdebug(void) __attribute__((noreturn));
static void dumpdebug(void) {
  int i;
  char erwhatbuf[100];
  
  printf("protocol: debug\n"
	 "local:    %08lx == %s\n"
	 "peer:     %08lx == %s\n"
	 "mtu:      %ld\n"
	 "routes:   %d\n",
	 localaddr, localtxt,
	 peeraddr, peertxt,
	 mtu,
	 nexroutes);
  for (i=0; i<nexroutes; i++) {
    sprintf(erwhatbuf, "route#%d:", i);
    printf("%-9s %08lx/%08lx == %s/%s\n",
	   erwhatbuf,
	   exroutes[i].prefix, exroutes[i].mask,
	   exroutes[i].prefixtxt, exroutes[i].masktxt);
  }
  if (ferror(stdout) || fclose(stdout)) sysfatal("flush stdout");
  exit(0);
}


static int task(const char *desc) {
  pid_t pid, pidr;
  int status;

  pid= fork();
  if (pid == (pid_t)-1) sysfatal("fork for task");
  if (!pid) return 1;

  for (;;) {
    pidr= waitpid(pid,&status,0);
    if (pidr!=(pid_t)-1) break;
    if (errno==EINTR) continue;
    sysfatal("waitpid for task");
  }
  assert(pidr==pid);

  if (WIFEXITED(status)) {
    if (WEXITSTATUS(status))
      fatal("userv-ipif service: %s exited with error exit status %d\n",
	    desc, WEXITSTATUS(status));
  } else if (WIFSIGNALED(status)) {
    fatal("userv-ipif service: %s died due to signal %s%s\n",
	  desc, strsignal(WTERMSIG(status)),
	  WCOREDUMP(status) ? " (core dumped)" : "");
  } else {
    fatal("userv-ipif service: %s unexpectedly terminated"
	  " with unknown status code %d\n", desc, status);
  }

  return 0;
}

static void createif(void) {
  struct ifreq ifr;
  int r;

  memset(&ifr,0,sizeof(ifr));
  ifr.ifr_flags= IFF_TUN | IFF_NO_PI;

  assert(sizeof(ifr.ifr_name) >= strlen(ifnamepat)+1);
  strcpy(ifr.ifr_name, ifnamepat);

  tunfd= open("/dev/net/tun", O_RDWR);
  if (!tunfd) sysfatal("open /dev/net/tun");

  r= fcntl(tunfd, F_GETFD);
  if (r==-1) sysfatal("fcntl(tunfd,F_GETFD)");
  r= fcntl(tunfd, F_SETFD, r|FD_CLOEXEC);
  if (r==-1) sysfatal("fcntl(tunfd,F_SETFD,|FD_CLOEXEC)");

  r= ioctl(tunfd, TUNSETIFF, (void*)&ifr);
  if (r) sysfatal("ioctl TUNSETIFF");

  /* ifr.ifr_name might not be null-terminated.  crazy abi. */
  ifname= malloc(sizeof(ifr.ifr_name)+1);
  if (!ifname) sysfatal("malloc for interface name");
  memcpy(ifname, ifr.ifr_name, sizeof(ifr.ifr_name));
  ifname[sizeof(ifr.ifr_name)]= 0;
}

static void netconfigure(void) {
  char mtutxt[100];
  int i;

  if (task("ifconfig")) {
    sprintf(mtutxt,"%lu",mtu);
  
    execlp("ifconfig", "ifconfig", ifname, localtxt,
	   "netmask","255.255.255.255", "pointopoint",peertxt, "-broadcast",
	   "mtu",mtutxt, "up", (char*)0);
    sysfatal("cannot exec ifconfig");
  }

  for (i=0; i<nexroutes; i++) {
    if (task("route")) {
      execlp("route","route", "add", "-net",exroutes[i].prefixtxt,
	     "netmask",exroutes[i].masktxt,
	     "gw",peertxt, "dev",ifname, (char*)0);
      sysfatal("cannot exec route (for route)");
    }
  }
}

static void setnonblock(int fd) {
  int r;
  r= fcntl(fd,F_GETFL); 
  if (r==-1) sysfatal("fcntl F_GETFL");
  r= fcntl(fd,F_SETFL, r|O_NONBLOCK);
  if (r==-1) sysfatal("fcntl F_SETFL O_NONBLOCK");
}

static void rx_packet(const uint8_t *packet, int len) {
  if (!len)
    return;
  for (;;) {
    int r= write(tunfd, packet, len);
    if (r<0) {
      if (errno==EINTR) continue;
      if (errno==EAGAIN || errno==ENOMEM) return; /* oh well */
      sysfatal("error writing packet to tun (transmitting)");
    }
    assert(r==len);
    return;
  }
}

static int output_waiting, input_waiting;

#define SLIP_END     0300
#define SLIP_ESC     0333
#define SLIP_ESC_END 0334
#define SLIP_ESC_ESC 0335

static void more_rx_data(uint8_t *input_buf, uint8_t *output_buf) {
  /* we make slip_data never contain continuation of a packet */
  /* input_buf is passed as a parameter since it's in copydata's stack frame */
  static int scanned;
  static int output_len;

  uint8_t *op= output_buf + output_len;
  const uint8_t *ip= input_buf + scanned;
  const uint8_t *ip_end= input_buf + input_waiting;
  int eaten= 0;
  
  for (;;) {
    if (ip>=ip_end) break;
    uint8_t c= *ip++;
    if (c==SLIP_END) {
      rx_packet(output_buf, op-output_buf);
      op= output_buf;
      eaten= ip - input_buf;
      continue;
    }
    if (c==SLIP_ESC) {
      if (ip>=ip_end) { /* rescan this when there's more */ ip--; break; }
      c= *ip++;
      if (c==SLIP_ESC_END) c=SLIP_END;
      else if (c==SLIP_ESC_ESC) c=SLIP_ESC;
      else fatal("unexpected byte 0%o after SLIP_ESC",c);
    }
    if (op == output_buf+mtu)
      fatal("SLIP packet exceeds mtu");
    *op++= c;
  }

  output_len= op - output_buf;
  scanned= ip - input_buf;

  input_waiting -= eaten;
  memmove(input_buf, input_buf+eaten, input_waiting);
  scanned -= eaten;
}

static void tx_packet(uint8_t *output_buf, const uint8_t *ip, int inlen) {
  /* output_buf is passed as a parameter since it's in copydata's stack frame */
  assert(!output_waiting);
  uint8_t *op= output_buf;

  *op++= SLIP_END;
  while (inlen-- >0) {
    uint8_t c= *ip++;
    if (c==SLIP_END) { *op++= SLIP_ESC; *op++= SLIP_ESC_END; }
    else if (c==SLIP_ESC) { *op++= SLIP_ESC; *op++= SLIP_ESC_ESC; }
    else *op++= c;
  }
  *op++= SLIP_END;
  assert(op <= output_buf + mtu*2+2);

  output_waiting= op - output_buf;
}

static void copydata(void) __attribute__((noreturn));
static void copydata(void) {
  uint8_t output_buf[mtu*2+2];
  uint8_t input_buf[mtu*2+2];
  uint8_t rx_packet_buf[mtu];

  int r, i;

  struct pollfd polls[3];
  memset(polls, 0, sizeof(polls));
  
  polls[0].fd= 0;      polls[0].events= POLLIN;
  polls[1].fd= 1;
  polls[2].fd= tunfd;

  /* We don't do flow control on input packets; instead, we just throw
   * away ones which the kernel doesn't accept.  So we always poll for
   * those.
   *
   * Output packets we buffer, so we poll only as appropriate for those.
   */

  /* Start by transmitting one END byte to say we're ready. */
  output_buf[0]= SLIP_END;
  output_waiting= 1;

  for (;;) {
    if (output_waiting) {
      r= write(1, output_buf, output_waiting);
      if (r<0) {
	if (errno==EINTR) continue;
	if (errno!=EAGAIN)
	  sysfatal("error writing SLIP output (packets being received)");
      } else {
	assert(r>0);
	output_waiting -= r;
	memmove(output_buf, output_buf+r, output_waiting);
      }
    }
    if (output_waiting) {
      polls[1].events |= POLLOUT;
      polls[2].events &= ~POLLIN;
    } else {
      polls[1].events &= ~POLLOUT;
      polls[2].events |= POLLIN;
    }
    r= poll(polls,3,-1);

    if (r<0) {
      if (errno==EINTR) continue;
      sysfatal("poll() failed");
    }
    assert(r>0); /* we used an infinite timeout */

    for (i=0; i<sizeof(polls)/sizeof(polls[0]); i++)
      if (polls[i].revents & ~polls[i].events)
	fatal("unexpected revents 0x%x for fd=%d",
	      polls[i].revents, polls[i].fd);

    if (polls[0].events & POLLIN) {
      int want= sizeof(input_buf) - input_waiting;
      if (want<0) fatal("incoming packet necessarily exceeds MTU");
      r= read(0, input_buf + input_waiting, want);
      if (r>0) {
	input_waiting += r;
	assert(input_waiting <= sizeof(input_buf));
	more_rx_data(input_buf, rx_packet_buf);
      } else if (r==0) {
	terminate(0);
      } else {
	if (!(errno==EINTR || errno==EAGAIN))
	  sysfatal("error reading input SLIP data (packets to transmit)");
      }
    }

    /* We handle what would be (polls[1].events & POLLOUT) above,
     * unconditionally.  That eliminates the need to poll in the usual case */
      
    if (polls[2].events & POLLIN) {
      uint8_t packet_buf[mtu];
      r= read(tunfd, packet_buf, mtu);
      if (r>0) {
	tx_packet(output_buf, packet_buf, r);
      } else {
	assert(r<0);
	if (!(errno==EAGAIN || errno==EWOULDBLOCK))
	  sysfatal("error reading packet (being transmitted) from tun");
      }
    }
  } 
}

int main(int argc, const char *const *argv) {
  parseargs(argc,argv);
  pconfig(configstr,0);
  checkpermit();
  if (!proto) dumpdebug();

  createif();
  netconfigure();
  setnonblock(tunfd);
  setnonblock(0);
  setnonblock(1);
  copydata();
}
