adrop / main.c
bind sockets then drop privleges, passing the file descriptors to a command
git clone http://git.nthia.dev/adrop

// public domain
// to build: gcc main.c -O3 -o adrop
const char *VERSION = "1.0.0";

#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <pwd.h>
#include <grp.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <netdb.h>

extern char **environ;
enum {
  ADROP_EUSAGE = 1,
  ADROP_EUNIMPLEMENTED = 2,
  ADROP_EMEMORY = 3,
  ADROP_EGETADDRINFO = 4,
  ADROP_EGETPWNAM = 5,
  ADROP_EGETGRNAM = 6,
  ADROP_EBIND = 7,
  ADROP_ECOMMAND_NOT_FOUND = 8,
  ADROP_EFORK = 9,
};

void usage(char *cmd, int code) {
  printf(
    "Usage: %s {OPTIONS} [SOCKETS..] -- [COMMAND..]\n"
    "\n"
    "  Bind sockets then drop privileges, passsing the file descriptors to COMMAND.\n"
    "\n"
    "Quick examples:\n"
    "\n"
    "  Bound ports with positional arguments:\n"
    "\n"
    "    sudo %s -u www-daemon -g www --respawn 80 443 \\\n"
    "      -- node server.js --http-fd={} --https-fd={}\n"
    "\n"
    "  Bound ports with named arguments:\n"
    "\n"
    "    sudo %s -u email:email imap_plain=143 imap_ssl=993 smtp0=25 smtp1=587 smtp2=2525 \\\n"
    "      -- ./smtp --smtp-plain={smtp0},{smtp1},{smtp2} \\\n"
    "        --imap-plain={imap_plain} --imap-ssl={imap_ssl}\n"
    "\n"
    "  Bound ipv4 and ipv6 ports with indexed arguments:\n"
    "\n"
    "    sudo %s -u xyzw [::1]:700 127.0.0.1:701 \\\n"
    "      -- python main.py --ipv4_fd={0} --ipv6_fd={1}\n"
    "\n"
    "OPTIONS are:\n"
    "\n"
    "  -h --help\n"
    "\n"
    "    Print this message and exit.\n"
    "\n"
    "  --version\n"
    "\n"
    "    Print the version number of this software and exit.\n"
    "\n"
    "  -u USER, --user=USER\n"
    "\n"
    "    Call setuid() with USER as an id or name.\n"
    "\n"
    "  -u USER:GROUP, --user=USER:GROUP\n"
    "\n"
    "    Call setuid() with USER and setgid() with GROUP, both as an id or name.\n"
    "\n"
    "  -g GROUP --group=GROUP\n"
    "\n"
    "    Call setgid() with GROUP as an id or name.\n"
    "\n"
    "  -r --respawn\n"
    "\n"
    "    When COMMAND terminates, run the COMMAND again.\n"
    "\n"
    "  -q --quiet\n"
    "\n"
    "    Suppress output when a process exits in respawn mode.\n"
    "\n"
    "  -d DELAY --delay=DELAY\n"
    "\n"
    "    Wait DELAY seconds between retries when --respawn is given. Default: 1.0\n"
    "\n"
    "  -e KEY=VALUE --env KEY=VALUE\n"
    "\n"
    "    Set an environment variable KEY as VALUE when executing COMMAND.\n"
    "\n"
    "  -E --preserve-env\n"
    "\n"
    "    Pass through all environment variables to COMMAND.\n"
    "    By default, no environment variables are passed through.\n"
    "\n"
    "Each SOCKET is of the form:\n"
    "\n"
    "  HOST:PORT   bind tcp port with explicit AF_INET or AF_INET6 address\n"
    "  PORT        bind tcp port on 0.0.0.0\n"
    "  HOST:uPORT  bind udp port with explicit AF_INET or AF_INET6 address\n"
    "  uPORT       bind udp port on 0.0.0.0\n"
    "  /PATH       AF_UNIX tcp socket with absolute path\n"
    "  ./PATH      AF_UNIX tcp socket with relative path\n"
    "  udp:/PATH   AF_UNIX udp socket with absolute path\n"
    "  udp:./PATH  AF_UNIX udp socket with relative path\n"
    "\n"
    "  Each SOCKET may be prepended by \"KEY=\" for a named \"{KEY}\" instead of\n"
    "  a positional substitution in COMMAND.\n"
    "\n"
    "  To provide multiple HOST addresses for the same PORT, provide HOST:PORT\n"
    "  pairs multiple times with the same PORT, one for each HOST.\n"
    "\n"
    "COMMAND is a program to execute with a list of arguments:\n"
    "\n"
    "  COMMAND will be run via exec() to take over file descriptors after running\n"
    "  setuid() and/or setgid() or via fork() if --respawn is used.\n"
    "\n"
    "  The COMMAND string may contain the special string \"{}\" which will be\n"
    "  replaced with the corresponding file descriptor for SOCKET in the order\n"
    "  provided in the SOCKETS section, left to right.\n"
    "\n"
    "  You can also use {N} to get at the Nth (starting from 0) file descriptor\n"
    "  or {KEY} to get at a socket with a name given by KEY=.\n"
    "\n",
    cmd, cmd, cmd, cmd
  );
  exit(code);
}

int is_bin(struct stat *s, char *file, uid_t uid, gid_t gid) {
  if (stat(file, s) != 0) return 0;
  if (s->st_mode & S_IFDIR) return 0;
  if (s->st_mode & S_IXOTH) return 1;
  if (s->st_gid == gid && (s->st_mode & S_IXGRP)) return 1;
  if (s->st_uid == uid && (s->st_mode & S_IXUSR)) return 1;
  return 0;
}

char *which(char *cmd) {
  if (cmd[0] == 0) return NULL;
  struct stat s;
  uid_t uid = geteuid();
  gid_t gid = getegid();
  if (cmd[0] == '/' || (cmd[0] == '.' && (cmd[1] == '.' || cmd[1] == '/'))) {
    return is_bin(&s, cmd, uid, gid) ? cmd : NULL;
  }
  char *path = getenv("PATH");
  size_t dlen = 1024 + strlen(cmd) + 2;
  char *d = malloc(dlen);
  if (d == NULL) {
    dprintf(1, "malloc(%lu) failed\n", dlen);
    exit(ADROP_EMEMORY);
  }
  size_t start = 0;
  for (size_t i = 0; i < 1024-start && path[i]; i++) {
    if (path[i] == ':' || path[i+1] == 0) {
      size_t n = i-start;
      memcpy(d, path+start, n);
      start = i+1;
      d[n] = '/';
      strcpy(d+n+1, cmd);
      if (is_bin(&s, d, uid, gid)) return d;
    }
  }
  return NULL;
}

int lookup(char **keys, size_t key_count, char *src, size_t src_len) {
  for (int i = 0; i < key_count; i++) {
    if (keys[i] == NULL) continue;
    char c = src[src_len];
    src[src_len] = 0;
    int n = strcmp(keys[i], src);
    src[src_len] = c;
    if (n == 0) return i;
  }
  return -1;
}

int max(int a, int b) { return a > b ? a : b; }

int interpolate_arg(char *dst, size_t dst_cap, char *str, int *fds, size_t fd_count, char **keys) {
  int inside = 0;
  char digits[12];
  size_t start = 0, offset = 0, key_index = 0;
  int i = 0, n = 0;
  for (; str[i]; i++) {
    char c = str[i];
    if (inside && c == '}') {
      inside = 0;
      if (start == i) {
        n = sprintf(digits, "%d", fds[key_index++]);
        if (offset+n >= dst_cap) return -1;
        memcpy(dst+offset, digits, n);
        offset += n;
      } else if ('0' <= str[start] && str[start] <= '9') {
        if (i-start < 8) {
          memcpy(digits, str+start, i-start);
          digits[i-start] = 0;
          int ix = atoi(digits);
          if (ix >= fd_count) continue;
          n = sprintf(digits, "%d", fds[ix]);
          if (offset+n >= dst_cap) return -1;
          memcpy(dst+offset, digits, n);
          offset += n;
       }
      } else if ((n = lookup(keys, fd_count, str+start, i-start)) >= 0) {
        if (n >= fd_count) return -1;
        n = sprintf(digits, "%d", fds[n]);
        if (offset+n >= dst_cap) return -1;
        memcpy(dst+offset, digits, n);
        offset += n;
      } else {
        str[i] = 0;
        dprintf(1,"key \"%s\" not set\n", str+start);
        exit(ADROP_EUSAGE);
      }
      start = i+1;
    } else if (inside) {
      continue;
    } else if (c == '\\') {
      if (offset+i-start >= dst_cap) return -1;
      memcpy(dst+offset, str+start, i-start);
      offset += i-start;
      start = i+1;
      i++;
      continue;
    } else if (c == '{') {
      if (offset+i-start >= dst_cap) return -1;
      inside = 1;
      memcpy(dst+offset, str+start, i-start);
      offset += i-start;
      start = i+1;
      continue;
    }
  }
  if (offset+i-start+1 >= dst_cap) return -1;
  memcpy(dst+offset, str+start, i-start);
  offset += i-start;
  dst[offset++] = 0;
  return offset;
}

void push_env(char **env, size_t *env_size, size_t *env_cap, char *kv) {
  if ((*env_size)+2 >= *env_cap) {
    (*env_cap) += 256;
    if (reallocarray(env, sizeof(void*), *env_cap) == NULL) {
      dprintf(1, "reallocarray(env, %lu, %lu) failed\n", sizeof(void*), *env_cap);
      exit(ADROP_EMEMORY);
    }
  }
  env[(*env_size)++] = kv;
  env[*env_size] = NULL;
}
void set_env(char **env, size_t *env_size, size_t *env_cap, char *kv) {
  int eq_i = 0;
  for (; kv[eq_i] && kv[eq_i] != '='; eq_i++);
  for (int i = 0; i < *env_size; i++) {
    if (strncmp(env[i], kv, eq_i+1) == 0) {
      env[i] = kv;
      return;
    }
  }
  push_env(env, env_size, env_cap, kv);
}

int bind_inet(char *host, int port, int socktype) {
  struct addrinfo hints, *ai_rp, *ai_result;
  memset(&hints, 0, sizeof(hints));
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = socktype;
  hints.ai_flags = AI_PASSIVE;
  hints.ai_protocol = 0;
  hints.ai_canonname = NULL;
  hints.ai_addr = NULL;
  hints.ai_next = NULL;
  if (getaddrinfo(host, NULL, &hints, &ai_result) != 0) {
    dprintf(1, "getaddrinfo() failed for host %s\n", host);
    exit(ADROP_EGETADDRINFO);
  }
  int fd = -1;
  for (ai_rp = ai_result; ai_rp != NULL; ai_rp = ai_rp->ai_next) {
    fd = socket(ai_rp->ai_family, SOCK_STREAM, 0);
    if (fd < 0) continue;
    ((struct sockaddr_in*) ai_rp->ai_addr)->sin_port = port;
    if (bind(fd, ai_rp->ai_addr, ai_rp->ai_addrlen) < 0) continue;
    return fd;
  }
  return -1;
}

int bind_unix(char *path, int socktype) {
  struct sockaddr_un addr;
  int fd = socket(AF_UNIX, socktype, 0);
  if (fd < 0) return -1;
  memset(&addr, 0, sizeof(addr));
  addr.sun_family = AF_UNIX;
  strncpy(addr.sun_path, path, sizeof(addr.sun_path)-1);
  if (bind(fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) return -1;
  return fd;
}


void drop(char *user, char *group) {
  u_int64_t uid;
  if (user != NULL && sscanf(user, "%lu", &uid)) {
    setuid(atoi(user));
  } else if (user != NULL) {
    struct passwd pw, *pw_result;
    size_t bufsize = sysconf(_SC_GETPW_R_SIZE_MAX);
    char *buf = malloc(bufsize);
    if (buf == NULL) {
      dprintf(1, "malloc(%lu) failed\n", bufsize);
      exit(ADROP_EMEMORY);
    }
    int s = getpwnam_r(user, &pw, buf, bufsize, &pw_result);
    if (s == 0 || pw_result == NULL) {
      dprintf(1, "getpwnam lookup failed for user %s\n", user);
      exit(ADROP_EGETPWNAM);
    }
    setuid(pw.pw_uid);
  }
  u_int64_t gid;
  if (group != NULL && sscanf(group, "%lu", &gid)) {
    setgid(atoi(group));
  } else if (group != NULL) {
    struct group gr, *gr_result;
    size_t bufsize = sysconf(_SC_GETGR_R_SIZE_MAX);
    char *buf = malloc(bufsize);
    if (buf == NULL) {
      dprintf(1, "malloc(%lu) failed\n", bufsize);
      exit(ADROP_EMEMORY);
    }
    int s = getgrnam_r(group, &gr, buf, bufsize, &gr_result);
    if (s == 0 || gr_result == NULL) {
      dprintf(1, "getgrnam lookup failed for group %s\n", group);
      exit(ADROP_EGETGRNAM);
    }
    setgid(gr.gr_gid);
  }
}

int main(int argc, char **argv) {
  if (argc <= 1) usage(argv[0], ADROP_EUSAGE);
  char *user = NULL, *group = NULL, *key = NULL, *host = NULL,
    *u = NULL, *g = NULL, *key_data = NULL, **keys = NULL;
  int port = -1, n = 0, respawn = 0, delay = 1000, quiet = 0;
  int *fds = NULL;
  size_t cmd_env_len = 0, cmd_env_cap = 256;
  char **cmd_env = calloc(sizeof(void*), cmd_env_cap);
  if (cmd_env == NULL) {
    dprintf(1, "calloc(%lu,%lu) failed\n", sizeof(void*), cmd_env_cap);
    exit(ADROP_EMEMORY);
  }
  cmd_env[0] = NULL;
  int fd_count = 0, fd_index = 0;
  int key_len = 0, key_index = 0;
  for (int i = 1; i < argc; i++) {
    if (strcmp(argv[i],"--preserve-env") == 0 || strcmp(argv[i],"-E") == 0) {
      for (int j = 0; environ[j]; j++) {
        push_env(cmd_env, &cmd_env_len, &cmd_env_cap, environ[j]);
      }
    }
    if (strcmp(argv[i],"--") == 0) {
      i++;
      break;
    }
    if (argv[i][0] == '-') continue;
    if ((n = sscanf(argv[i], "%m[^=]=%m[^:]:%d", &key, &host, &port)) >= 2) {
      key_len += n;
    } else {
      key_len++;
    }
    fd_count++;
  }
  key_data = malloc(key_len);
  if (key_data == NULL) {
    dprintf(1, "malloc(%d) failed\n", key_len);
    exit(ADROP_EMEMORY);
  }
  keys = calloc(sizeof(char*), fd_count);
  if (keys == NULL) {
    dprintf(1, "calloc(%lu,%u) failed\n", sizeof(void*), key_len);
    exit(ADROP_EMEMORY);
  }
  fds = calloc(sizeof(int*), fd_count);
  if (keys == NULL) {
    dprintf(1, "calloc(%lu,%u) failed\n", sizeof(int*), fd_count);
    exit(ADROP_EMEMORY);
  }
  int i = 1;
  char c = 0;
  for (; i < argc; i++) {
    if (strcmp(argv[i],"--") == 0) {
      break;
    } else if (strcmp(argv[i],"-h") == 0 || strcmp(argv[i],"--help") == 0) {
      usage(argv[0], 0);
    } else if (strcmp(argv[i],"--version") == 0) {
      printf("%s\n", VERSION);
      exit(0);
    } else if (strcmp(argv[i],"-u") == 0 || strcmp(argv[i],"--user") == 0) {
      if (i+1 < argc) {
        user = argv[++i];
        if ((n = sscanf(user, "%m[^:]:%m[^:]", &u, &g)) == 2) {
          user = u;
          group = g;
        }
      }
    } else if (strncmp(argv[i],"--user=",7) == 0) {
      user = argv[i]+7;
      if ((n = sscanf(user, "%m[^:]:%m[^:]", &u, &g)) == 2) {
        user = u;
        group = g;
      }
    } else if (strcmp(argv[i],"-g") == 0 || strcmp(argv[i],"--group") == 0) {
      if (i+1 < argc) group = argv[++i];
    } else if (strncmp(argv[i],"--group=",8) == 0) {
      group = argv[i]+8;
    } else if (strcmp(argv[i],"-r") == 0 || strcmp(argv[i],"--respawn") == 0) {
      respawn = 1;
    } else if (strcmp(argv[i],"-d") == 0 || strcmp(argv[i],"--delay") == 0) {
      if (i+1 < argc) delay = (int) (atof(argv[++i])*1000+0.5);
    } else if (strncmp(argv[i],"--delay=",8) == 0) {
      delay = (int) (atof(argv[i]+8)*1000+0.5);
    } else if (strcmp(argv[i],"-q") == 0 || strcmp(argv[i],"--quiet") == 0) {
      quiet = 1;
    } else if (strcmp(argv[i],"-e") == 0 || strcmp(argv[i],"--env") == 0) {
      if (i+1 < argc) set_env(cmd_env, &cmd_env_len, &cmd_env_cap, argv[++i]);
    } else if (strncmp(argv[i],"--env=",6) == 0) {
      set_env(cmd_env, &cmd_env_len, &cmd_env_cap, argv[i]+6);
    } else if (argv[i][0] == '-') {
      continue;
    } else if ((n = sscanf(argv[i], "%m[^=]=%[./]%m[^:]", &key, &c, &host)) == 3) {
      keys[key_index++] = key;
      size_t hlen = strlen(host);
      char *path = malloc(2+hlen);
      path[0] = c;
      memcpy(path+1, host, hlen);
      path[1+hlen] = 0;
      int fd = bind_unix(path, SOCK_STREAM);
      if (fd < 0) {
        dprintf(1, "bind() failed for %s\n", path);
        exit(ADROP_EBIND);
      }
      fds[fd_index++] = fd;
      free(path);
      free(host);
    } else if ((n = sscanf(argv[i], "%m[^=]=udp:%[./]%m[^:]", &key, &c, &host)) == 3) {
      keys[key_index++] = key;
      size_t hlen = strlen(host);
      char *path = malloc(2+hlen);
      path[0] = c;
      memcpy(path+1, host, hlen);
      path[1+hlen] = 0;
      int fd = bind_unix(path, SOCK_DGRAM);
      if (fd < 0) {
        dprintf(1, "udp bind() failed for %s\n", path);
        exit(ADROP_EBIND);
      }
      fds[fd_index++] = fd;
      free(path);
      free(host);
    } else if ((n = sscanf(argv[i], "%m[^=]=tcp:%[./]%m[^:]", &key, &c, &host)) == 3) {
      keys[key_index++] = key;
      size_t hlen = strlen(host);
      char *path = malloc(2+hlen);
      path[0] = c;
      memcpy(path+1, host, hlen);
      path[1+hlen] = 0;
      int fd = bind_unix(path, SOCK_STREAM);
      if (fd < 0) {
        dprintf(1, "bind() failed for %s\n", path);
        exit(ADROP_EBIND);
      }
      fds[fd_index++] = fd;
      free(path);
      free(host);
    } else if (argv[i][0] == '.' || argv[i][0] == '/') {
      keys[key_index++] = NULL;
      int fd = bind_unix(argv[i], SOCK_STREAM);
      if (fd < 0) {
        dprintf(1, "bind() failed for %s\n", argv[i]);
        exit(ADROP_EBIND);
      }
      fds[fd_index++] = fd;
    } else if (strncmp(argv[i],"udp:/",5) == 0 || strncmp(argv[i],"udp:.",5) == 0) {
      keys[key_index++] = NULL;
      int fd = bind_unix(argv[i]+4, SOCK_DGRAM);
      if (fd < 0) {
        dprintf(1, "udp bind() failed for %s\n", argv[i]+4);
        exit(ADROP_EBIND);
      }
      fds[fd_index++] = fd;
    } else if (strncmp(argv[i],"tcp:/",5) == 0 || strncmp(argv[i],"tcp:.",5) == 0) {
      keys[key_index++] = NULL;
      int fd = bind_unix(argv[i]+4, SOCK_STREAM);
      if (fd < 0) {
        dprintf(1, "bind() failed for %s\n", argv[i]+4);
        exit(ADROP_EBIND);
      }
      fds[fd_index++] = fd;
    } else if ((n = sscanf(argv[i], "%m[^=]=%m[^:]:%d", &key, &host, &port)) == 3) {
      keys[key_index++] = key;
      int fd = bind_inet(host, htons(port), SOCK_STREAM);
      if (fd < 0) {
        dprintf(1, "bind() failed for %s:%d\n", host, port);
        exit(ADROP_EBIND);
      }
      fds[fd_index++] = fd;
      free(host);
    } else if ((n = sscanf(argv[i], "%m[^=]=[%m[^]]]:%d", &key, &host, &port)) == 3) {
      keys[key_index++] = key;
      int fd = bind_inet(host, htons(port), SOCK_STREAM);
      if (fd < 0) {
        dprintf(1, "bind() failed for %s:%d\n", host, port);
        exit(ADROP_EBIND);
      }
      fds[fd_index++] = fd;
      free(host);
    } else if ((n = sscanf(argv[i], "%m[^=]=%d", &key, &port)) == 2) {
      keys[key_index++] = key;
      int fd = bind_inet("0.0.0.0", htons(port), SOCK_STREAM);
      if (fd < 0) {
        dprintf(1, "bind() failed for 0.0.0.0:%d\n", port);
        exit(ADROP_EBIND);
      }
      fds[fd_index++] = fd;
    } else if ((n = sscanf(argv[i], "%m[^=]=%m[^:]:u%d", &key, &host, &port)) == 3) {
      keys[key_index++] = key;
      int fd = bind_inet(host, htons(port), SOCK_DGRAM);
      if (fd < 0) {
        dprintf(1, "bind() failed for %s:%d\n", host, port);
        exit(ADROP_EBIND);
      }
      fds[fd_index++] = fd;
      free(host);
    } else if ((n = sscanf(argv[i], "%m[^=]=[%m[^]]]:u%d", &key, &host, &port)) == 3) {
      keys[key_index++] = key;
      int fd = bind_inet(host, htons(port), SOCK_DGRAM);
      if (fd < 0) {
        dprintf(1, "bind() failed for [%s]:%d\n", host, port);
        exit(ADROP_EBIND);
      }
      fds[fd_index++] = fd;
      free(host);
    } else if ((n = sscanf(argv[i], "%m[^:]:%d", &host, &port)) == 2) {
      keys[key_index++] = NULL;
      int fd = bind_inet(host, htons(port), SOCK_STREAM);
      if (fd < 0) {
        dprintf(1, "bind() failed for %s:%d\n", host, port);
        exit(ADROP_EBIND);
      }
      fds[fd_index++] = fd;
    } else if ((n = sscanf(argv[i], "[%m[^]]]:%d", &host, &port)) == 2) {
      keys[key_index++] = NULL;
      int fd = bind_inet(host, htons(port), SOCK_STREAM);
      if (fd < 0) {
        dprintf(1, "bind() failed for [%s]:%d\n", host, port);
        exit(ADROP_EBIND);
      }
      fds[fd_index++] = fd;
    } else if ((n = sscanf(argv[i], "%m[^:]:u%d", &host, &port)) == 2) {
      keys[key_index++] = NULL;
      int fd = bind_inet(host, htons(port), SOCK_DGRAM);
      if (fd < 0) {
        dprintf(1, "bind() failed for %s:%d\n", host, port);
        exit(ADROP_EBIND);
      }
      fds[fd_index++] = fd;
    } else if ((n = sscanf(argv[i], "[%m[^]]]:u%d", &host, &port)) == 2) {
      keys[key_index++] = NULL;
      int fd = bind_inet(host, htons(port), SOCK_DGRAM);
      if (fd < 0) {
        dprintf(1, "bind() failed for [%s]:%d\n", host, port);
        exit(ADROP_EBIND);
      }
      fds[fd_index++] = fd;
    } else if ((n = sscanf(argv[i], "%d", &port)) == 1) {
      keys[key_index++] = NULL;
      int fd = bind_inet("0.0.0.0", htons(port), SOCK_STREAM);
      if (fd < 0) {
        dprintf(1, "bind() failed for 0.0.0.0:%d\n", port);
        exit(ADROP_EBIND);
      }
      fds[fd_index++] = fd;
    } else if ((n = sscanf(argv[i], "u%d", &port)) == 1) {
      keys[key_index++] = NULL;
      int fd = bind_inet("0.0.0.0", htons(port), SOCK_DGRAM);
      if (fd < 0) {
        dprintf(1, "bind() failed for 0.0.0.0:%d\n", port);
        exit(ADROP_EBIND);
      }
      fds[fd_index++] = fd;
    } else {
      dprintf(1, "unexpected socket format: %s\n", argv[i]);
      exit(ADROP_EUSAGE);
    }
  }
  int cmd_start = i+1;
  if (cmd_start >= argc) {
    dprintf(1, "COMMAND not provided\n");
    exit(ADROP_EUSAGE);
  }

  if (!respawn) drop(user, group);

  char *cmd_file = which(argv[cmd_start]);
  if (cmd_file == NULL) {
    dprintf(1, "%s: command not found\n", argv[cmd_start]);
    exit(ADROP_ECOMMAND_NOT_FOUND);
  }
  int cmd_arg_len = argc-cmd_start;
  char **cmd_args = calloc(sizeof(char*), cmd_arg_len+1);
  if (cmd_args == NULL) {
    dprintf(1, "calloc(%lu,%d) failed\n", sizeof(char*), cmd_arg_len+1);
    exit(ADROP_EMEMORY);
  }
  cmd_args[0] = cmd_file;
  size_t buf_len = 1024*8;
  char *buf = malloc(buf_len);
  if (buf == NULL) {
    dprintf(1, "malloc(%lu) failed\n", buf_len);
    exit(ADROP_EMEMORY);
  }
  for (int i = 1; i < cmd_arg_len; i++) {
    n = interpolate_arg(buf, buf_len, argv[cmd_start+i], fds, fd_count, keys);
    if (n < 0) {
      dprintf(1, "interpolated argument too large (> %lu bytes)\n", buf_len);
      exit(ADROP_EUSAGE);
    }
    cmd_args[i] = malloc(n+1);
    if (cmd_args[i] == NULL) {
      dprintf(1, "malloc(%d) failed\n", n+1);
      exit(ADROP_EMEMORY);
    }
    strncpy(cmd_args[i], buf, n+1);
  }
  cmd_args[cmd_arg_len] = NULL;

  if (respawn) {
    int wstatus, nrespawn = 0;
    while(1) {
      pid_t pid = fork();
      if (pid < 0) {
        dprintf(1, "fork() failed\n");
        exit(ADROP_EFORK);
      } else if (pid == 0) {
        drop(user, group);
        execve(cmd_file, cmd_args, cmd_env);
      } else {
        waitpid(pid, &wstatus, 0);
        if (!quiet && WIFSIGNALED(wstatus)) {
          dprintf(1, "*** [%d] forked process exited with code %d from signal %d. sleeping for %0.3f seconds\n",
            nrespawn++, WEXITSTATUS(wstatus), WTERMSIG(wstatus), (float) delay / 1000.0);
        } else if (!quiet) {
          dprintf(1, "*** [%d] forked process exited with code %d. sleeping for %0.3f seconds\n",
            nrespawn++, WEXITSTATUS(wstatus), (float) delay / 1000.0);
        }
        if (delay >= 0) usleep(delay);
      }
    }
  } else {
    execve(cmd_file, cmd_args, cmd_env);
  }
  return 0;
}