Browse Source

Start tor-fw-helper in the background, and log whatever it outputs

Steven Murdoch 14 years ago
parent
commit
a6dc00fa75
8 changed files with 485 additions and 1 deletions
  1. 12 0
      doc/tor.1.txt
  2. 398 0
      src/common/util.c
  3. 15 0
      src/common/util.h
  4. 2 0
      src/or/config.c
  5. 12 0
      src/or/main.c
  6. 4 0
      src/or/or.h
  7. 41 0
      src/test/test_util.c
  8. 1 1
      src/tools/tor-fw-helper/tor-fw-helper.c

+ 12 - 0
doc/tor.1.txt

@@ -873,6 +873,18 @@ is non-zero):
     specified in ORPort. (Default: 0.0.0.0) This directive can be specified
     specified in ORPort. (Default: 0.0.0.0) This directive can be specified
     multiple times to bind to multiple addresses/ports.
     multiple times to bind to multiple addresses/ports.
 
 
+**PortForwarding** **0**|**1**::
+    Attempt to automatically forward the DirPort and ORPort on a NAT router
+    connecting this Tor server to the Internet. If set, Tor will try both
+    NAT-PMP (common on Apple routers) and UPnP (common on routers from other
+    manufacturers). (Default: 0)
+
+**PortForwardingHelper** __filename__|__pathname__::
+    If PortForwarding is set, use this executable to configure the forwarding.
+    If set to a filename, the system path will be searched for the executable.
+    If set to a path, only the specified path will be executed.
+    (Default: tor-fw-helper)
+
 **PublishServerDescriptor** **0**|**1**|**v1**|**v2**|**v3**|**bridge**|**hidserv**,**...**::
 **PublishServerDescriptor** **0**|**1**|**v1**|**v2**|**v3**|**bridge**|**hidserv**,**...**::
     This option is only considered if you have an ORPort defined. You can
     This option is only considered if you have an ORPort defined. You can
     choose multiple arguments, separated by commas.
     choose multiple arguments, separated by commas.

+ 398 - 0
src/common/util.c

@@ -14,6 +14,7 @@
 #define _GNU_SOURCE
 #define _GNU_SOURCE
 
 
 #include "orconfig.h"
 #include "orconfig.h"
+#define UTIL_PRIVATE
 #include "util.h"
 #include "util.h"
 #include "torlog.h"
 #include "torlog.h"
 #undef log
 #undef log
@@ -2877,3 +2878,400 @@ load_windows_system_library(const TCHAR *library_name)
   return LoadLibrary(path);
   return LoadLibrary(path);
 }
 }
 #endif
 #endif
+
+/** Format child_state and saved_errno as a hex string placed in hex_errno.
+ * Called between fork and _exit, so must be signal-handler safe */
+void
+format_helper_exit_status(unsigned char child_state, int saved_errno,
+                          char *hex_errno)
+{
+  /* Convert errno to be unsigned for hex conversion */
+  unsigned int unsigned_errno;
+  char *cur;
+
+  /* If errno is negative, negate it */
+  if (saved_errno < 0) {
+    unsigned_errno = (unsigned int) -saved_errno;
+  } else {
+    unsigned_errno = (unsigned int) saved_errno;
+  }
+
+  /* Convert errno to hex (start before \n) */
+  cur = hex_errno + HEX_ERRNO_SIZE - 2;
+
+  do {
+    *cur-- = "0123456789ABCDEF"[unsigned_errno % 16];
+    unsigned_errno /= 16;
+  } while (unsigned_errno != 0 && cur >= hex_errno);
+
+  /* Add on the minus side if errno was negative */
+  if (saved_errno < 0)
+    *cur-- = '-';
+
+  /* Leave a gap */
+  *cur-- = '/';
+
+  /* Convert child_state to hex */
+  do {
+    *cur-- = "0123456789ABCDEF"[child_state % 16];
+    child_state /= 16;
+  } while (child_state != 0 && cur >= hex_errno);
+}
+
+/* Maximum number of file descriptors, if we cannot get it via sysconf() */
+#define DEFAULT_MAX_FD 256
+
+#define CHILD_STATE_INIT 0
+#define CHILD_STATE_PIPE 1
+#define CHILD_STATE_MAXFD 2
+#define CHILD_STATE_FORK 3
+#define CHILD_STATE_DUPOUT 4
+#define CHILD_STATE_DUPERR 5
+#define CHILD_STATE_REDIRECT 6
+#define CHILD_STATE_CLOSEFD 7
+#define CHILD_STATE_EXEC 8
+#define CHILD_STATE_FAILEXEC 9
+
+#define SPAWN_ERROR_MESSAGE "ERR: Failed to spawn background process - code "
+
+/** Start a program in the background. If <b>filename</b> contains a '/', then
+ * it will be treated as an absolute or relative path.  Otherwise the system
+ * path will be searched for <b>filename</b>. Returns pid on success, otherwise
+ * returns -1.
+ * Some parts of this code are based on the POSIX subprocess module from Python
+ */
+static int
+tor_spawn_background(const char *const filename, int *stdout_read,
+                     int *stderr_read, const char **argv)
+{
+  pid_t pid;
+  int stdout_pipe[2];
+  int stderr_pipe[2];
+  int fd, retval;
+  ssize_t nbytes;
+
+  const char *error_message = SPAWN_ERROR_MESSAGE;
+  size_t error_message_length;
+
+  /* Represents where in the process of spawning the program is;
+     this is used for printing out the error message */
+  unsigned char child_state = CHILD_STATE_INIT;
+
+  char hex_errno[HEX_ERRNO_SIZE];
+
+  static int max_fd = -1;
+
+  /* We do the strlen here because strlen() is not signal handler safe,
+     and we are not allowed to use unsafe functions between fork and exec */
+  error_message_length = strlen(error_message);
+
+  /* Fill hex_errno with spaces, and a trailing newline */
+  memset(hex_errno, ' ', sizeof(hex_errno) - 1);
+  hex_errno[sizeof(hex_errno) - 1] = '\n';
+
+  child_state = CHILD_STATE_PIPE;
+
+  /* Set up pipe for redirecting stdout and stderr of child */
+  retval = pipe(stdout_pipe);
+  if (-1 == retval) {
+    log_err(LD_GENERAL,
+      "Failed to set up pipe for stdout communication with child process: %s",
+       strerror(errno));
+    return -1;
+  }
+
+  retval = pipe(stderr_pipe);
+  if (-1 == retval) {
+    log_err(LD_GENERAL,
+      "Failed to set up pipe for stderr communication with child process: %s",
+      strerror(errno));
+    return -1;
+  }
+
+  child_state = CHILD_STATE_MAXFD;
+
+#ifdef _SC_OPEN_MAX
+  if (-1 != max_fd) {
+    max_fd = (int) sysconf(_SC_OPEN_MAX);
+    if (max_fd == -1)
+      max_fd = DEFAULT_MAX_FD;
+      log_warn(LD_GENERAL,
+               "Cannot find maximum file descriptor, assuming %d", max_fd);
+  }
+#else
+  max_fd = DEFAULT_MAX_FD;
+#endif
+
+  child_state = CHILD_STATE_FORK;
+
+  pid = fork();
+  if (0 == pid) {
+    /* In child */
+
+    child_state = CHILD_STATE_DUPOUT;
+
+    /* Link child stdout to the write end of the pipe */
+    retval = dup2(stdout_pipe[1], STDOUT_FILENO);
+    if (-1 == retval)
+        goto error;
+
+    child_state = CHILD_STATE_DUPERR;
+
+    /* Link child stderr to the write end of the pipe */
+    retval = dup2(stderr_pipe[1], STDERR_FILENO);
+    if (-1 == retval)
+        goto error;
+
+    child_state = CHILD_STATE_REDIRECT;
+
+    /* Link stdin to /dev/null */
+    fd = open("/dev/null", O_RDONLY);
+    if (fd != -1)
+      dup2(STDIN_FILENO, fd);
+    else
+      goto error;
+
+    child_state = CHILD_STATE_CLOSEFD;
+
+    /* Close all other fds, including the read end of the pipe */
+    /* TODO: use closefrom if available */
+    for (fd = STDERR_FILENO + 1; fd < max_fd; fd++)
+      close(fd);
+
+    child_state = CHILD_STATE_EXEC;
+
+    /* Call the requested program. We need the cast because
+       execvp doesn't define argv as const, even though it
+       does not modify the arguments */
+    execvp(filename, (char *const *) argv);
+
+    /* If we got here, the exec or open(/dev/null) failed */
+
+    child_state = CHILD_STATE_FAILEXEC;
+
+  error:
+    /* TODO: are we leaking fds from the pipe? */
+
+    format_helper_exit_status(child_state, errno, hex_errno);
+
+    /* Write the error message. GCC requires that we check the return
+       value, but there is nothing we can do if it fails */
+    nbytes = write(STDOUT_FILENO, error_message, error_message_length);
+    nbytes = write(STDOUT_FILENO, hex_errno, sizeof(hex_errno));
+
+    _exit(255);
+    return -1; /* Never reached, but avoids compiler warning */
+  }
+
+  /* In parent */
+
+  if (-1 == pid) {
+    log_err(LD_GENERAL, "Failed to fork child process: %s", strerror(errno));
+    close(stdout_pipe[0]);
+    close(stdout_pipe[1]);
+    close(stderr_pipe[0]);
+    close(stderr_pipe[1]);
+    return -1;
+  }
+
+  /* Return read end of the pipes to caller, and close write end */
+  *stdout_read = stdout_pipe[0];
+  retval = close(stdout_pipe[1]);
+
+  if (-1 == retval) {
+    log_err(LD_GENERAL,
+            "Failed to close write end of stdout pipe in parent process: %s",
+            strerror(errno));
+    /* Do not return -1, because the child is running, so the parent
+       needs to know about the pid in order to reap it later */
+  }
+
+  *stderr_read = stderr_pipe[0];
+  retval = close(stderr_pipe[1]);
+
+  if (-1 == retval) {
+    log_err(LD_GENERAL,
+            "Failed to close write end of stderr pipe in parent process: %s",
+            strerror(errno));
+    /* Do not return -1, because the child is running, so the parent
+       needs to know about the pid in order to reap it later */
+  }
+
+  return pid;
+}
+
+/** Read from stream, and send lines to log at the specified log level.
+ * Returns 1 if stream is closed normally, -1 if there is a error reading, and
+ * 0 otherwise. Handles lines from tor-fw-helper and
+ * tor_spawn_background() specially.
+ */
+static int
+log_from_pipe(FILE *stream, int severity, const char *executable,
+              int *child_status)
+{
+  char buf[256];
+
+  for (;;) {
+    char *retval;
+    retval = fgets(buf, sizeof(buf), stream);
+
+    if (NULL == retval) {
+      if (feof(stream)) {
+        /* Program has closed stream (probably it exited) */
+        /* TODO: check error */
+        fclose(stream);
+        return 1;
+      } else {
+        if (EAGAIN == errno) {
+          /* Nothing more to read, try again next time */
+          return 0;
+        } else {
+          /* There was a problem, abandon this child process */
+          fclose(stream);
+          return -1;
+        }
+      }
+    } else {
+      /* We have some data, log it and keep asking for more */
+      size_t len;
+
+      len = strlen(buf);
+      if (buf[len - 1] == '\n') {
+        /* Remove the trailing newline */
+        buf[len - 1] = '\0';
+      } else {
+        /* No newline; check whether we overflowed the buffer */
+        if (!feof(stream))
+          log_err(LD_GENERAL,
+                  "Line from port forwarding helper was truncated: %s", buf);
+          /* TODO: What to do with this error? */
+      }
+
+      /* Check if buf starts with SPAWN_ERROR_MESSAGE */
+      if (strstr(buf, SPAWN_ERROR_MESSAGE) == buf) {
+          /* Parse error message */
+          int retval, child_state, saved_errno;
+          retval = sscanf(buf, SPAWN_ERROR_MESSAGE "%d/%d",
+                          &child_state, &saved_errno);
+          if (retval == 2) {
+              log_err(LD_GENERAL,
+                "Failed to start child process \"%s\" in state %d: %s",
+                executable, child_state, strerror(saved_errno));
+              if (child_status)
+                  *child_status = 1;
+          } else {
+              /* Failed to parse message from child process, log it as error */
+              log_err(LD_GENERAL,
+                "Unexpected message from port forwarding helper \"%s\": %s",
+                executable, buf);
+          }
+      } else {
+          log_fn(severity, LD_GENERAL, "Port forwarding helper says: %s", buf);
+      }
+    }
+  }
+
+  /* We should never get here */
+  return -1;
+}
+
+void
+tor_check_port_forwarding(const char *filename, int dir_port, int or_port,
+                          time_t now)
+{
+
+/* When fw-helper succeeds, how long do we wait until running it again */
+#define TIME_TO_EXEC_FWHELPER_SUCCESS 300
+/* When fw-helper fails, how long do we wait until running it again */
+#define TIME_TO_EXEC_FWHELPER_FAIL 60
+
+  static int child_pid = -1;
+  static FILE *stdout_read = NULL;
+  static FILE *stderr_read = NULL;
+  static time_t time_to_run_helper = 0;
+  int stdout_status, stderr_status, retval;
+  const char *argv[10];
+  char s_dirport[6], s_orport[6];
+
+  tor_assert(filename);
+
+  /* Set up command line for tor-fw-helper */
+  snprintf(s_dirport, sizeof s_dirport, "%d", dir_port);
+  snprintf(s_orport, sizeof s_orport, "%d", or_port);
+
+  /* TODO: Allow different internal and external ports */
+  argv[0] = filename;
+  argv[1] = "--internal-or-port";
+  argv[2] = s_orport;
+  argv[3] = "--external-or-port";
+  argv[4] = s_orport;
+  argv[5] = "--internal-dir-port";
+  argv[6] = s_dirport;
+  argv[7] = "--external-dir-port";
+  argv[8] = s_dirport;
+  argv[9] = NULL;
+
+  /* Start the child, if it is not already running */
+  if (-1 == child_pid &&
+      time_to_run_helper < now) {
+    int fd_out, fd_err;
+
+    /* Assume tor-fw-helper will succeed, start it later*/
+    time_to_run_helper = now + TIME_TO_EXEC_FWHELPER_SUCCESS;
+
+    child_pid = tor_spawn_background(filename, &fd_out, &fd_err, argv);
+    if (child_pid < 0) {
+      log_err(LD_GENERAL, "Failed to start port forwarding helper %s",
+              filename);
+      child_pid = -1;
+      return;
+    }
+    /* Set stdout/stderr pipes to be non-blocking */
+    fcntl(fd_out, F_SETFL, O_NONBLOCK);
+    fcntl(fd_err, F_SETFL, O_NONBLOCK);
+    /* Open the buffered IO streams */
+    stdout_read = fdopen(fd_out, "r");
+    stderr_read = fdopen(fd_err, "r");
+
+    log_info(LD_GENERAL,
+      "Started port forwarding helper (%s) with pid %d", filename, child_pid);
+  }
+
+  /* If child is running, read from its stdout and stderr) */
+  if (child_pid > 0) {
+    /* Read from stdout/stderr and log result */
+    retval = 0;
+    stdout_status = log_from_pipe(stdout_read, LOG_INFO, filename, &retval);
+    stderr_status = log_from_pipe(stderr_read, LOG_ERR, filename, &retval);
+    if (retval) {
+      /* There was a problem in the child process */
+      time_to_run_helper = now + TIME_TO_EXEC_FWHELPER_FAIL;
+    }
+  
+    /* Combine the two statuses in order of severity */
+    if (-1 == stdout_status || -1 == stderr_status)
+      /* There was a failure */
+      retval = -1;
+    else if (1 == stdout_status || 1 == stderr_status)
+      /* stdout or stderr was closed */
+      retval = 1;
+    else
+      /* Both are fine */
+      retval = 0;
+  
+    /* If either pipe indicates a failure, act on it */
+    if (0 != retval) {
+      if (1 == retval) {
+        log_info(LD_GENERAL, "Port forwarding helper terminated");
+      } else {
+        log_err(LD_GENERAL, "Failed to read from port forwarding helper");
+      }
+  
+      /* TODO: The child might not actually be finished (maybe it failed or
+         closed stdout/stderr), so maybe we shouldn't start another? */
+      child_pid = -1;
+    }
+  }
+}
+
+

+ 15 - 0
src/common/util.h

@@ -340,10 +340,25 @@ void start_daemon(void);
 void finish_daemon(const char *desired_cwd);
 void finish_daemon(const char *desired_cwd);
 void write_pidfile(char *filename);
 void write_pidfile(char *filename);
 
 
+/* Port forwarding */
+void tor_check_port_forwarding(const char *filename,
+                               int dir_port, int or_port, time_t now);
+
 #ifdef MS_WINDOWS
 #ifdef MS_WINDOWS
 HANDLE load_windows_system_library(const TCHAR *library_name);
 HANDLE load_windows_system_library(const TCHAR *library_name);
 #endif
 #endif
 
 
+#ifdef UTIL_PRIVATE
+/* Prototypes for private functions only used by util.c (and unit tests) */
+void format_helper_exit_status(unsigned char child_state,
+                               int saved_errno, char *hex_errno);
+
+/* Space for hex values of child state, a slash, saved_errno (with
+   leading minus) and newline (no null) */
+#define HEX_ERRNO_SIZE (sizeof(char) * 2 + 1 + \
+                        1 + sizeof(int) * 2 + 1)
+#endif
+
 const char *libor_get_digests(void);
 const char *libor_get_digests(void);
 
 
 #endif
 #endif

+ 2 - 0
src/or/config.c

@@ -317,6 +317,8 @@ static config_var_t _option_vars[] = {
   V(PerConnBWRate,               MEMUNIT,  "0"),
   V(PerConnBWRate,               MEMUNIT,  "0"),
   V(PidFile,                     STRING,   NULL),
   V(PidFile,                     STRING,   NULL),
   V(TestingTorNetwork,           BOOL,     "0"),
   V(TestingTorNetwork,           BOOL,     "0"),
+  V(PortForwarding,              BOOL,     "0"),
+  V(PortForwardingHelper,        FILENAME, "tor-fw-helper"),
   V(PreferTunneledDirConns,      BOOL,     "1"),
   V(PreferTunneledDirConns,      BOOL,     "1"),
   V(ProtocolWarnings,            BOOL,     "0"),
   V(ProtocolWarnings,            BOOL,     "0"),
   V(PublishServerDescriptor,     CSV,      "1"),
   V(PublishServerDescriptor,     CSV,      "1"),

+ 12 - 0
src/or/main.c

@@ -1026,6 +1026,7 @@ run_scheduled_events(time_t now)
   static time_t time_to_check_for_expired_networkstatus = 0;
   static time_t time_to_check_for_expired_networkstatus = 0;
   static time_t time_to_write_stats_files = 0;
   static time_t time_to_write_stats_files = 0;
   static time_t time_to_write_bridge_stats = 0;
   static time_t time_to_write_bridge_stats = 0;
+  static time_t time_to_check_port_forwarding = 0;
   static int should_init_bridge_stats = 1;
   static int should_init_bridge_stats = 1;
   static time_t time_to_retry_dns_init = 0;
   static time_t time_to_retry_dns_init = 0;
   or_options_t *options = get_options();
   or_options_t *options = get_options();
@@ -1385,6 +1386,17 @@ run_scheduled_events(time_t now)
 #define BRIDGE_STATUSFILE_INTERVAL (30*60)
 #define BRIDGE_STATUSFILE_INTERVAL (30*60)
     time_to_write_bridge_status_file = now+BRIDGE_STATUSFILE_INTERVAL;
     time_to_write_bridge_status_file = now+BRIDGE_STATUSFILE_INTERVAL;
   }
   }
+
+  if (time_to_check_port_forwarding < now &&
+      options->PortForwarding &&
+      server_mode(options)) {
+#define PORT_FORWARDING_CHECK_INTERVAL 5
+    tor_check_port_forwarding(options->PortForwardingHelper,
+                              options->DirPort,
+                              options->ORPort,
+                              now);
+    time_to_check_port_forwarding = now+PORT_FORWARDING_CHECK_INTERVAL;
+  }
 }
 }
 
 
 /** Timer: used to invoke second_elapsed_callback() once per second. */
 /** Timer: used to invoke second_elapsed_callback() once per second. */

+ 4 - 0
src/or/or.h

@@ -2772,6 +2772,10 @@ typedef struct {
                        * possible. */
                        * possible. */
   int PreferTunneledDirConns; /**< If true, avoid dirservers that don't
   int PreferTunneledDirConns; /**< If true, avoid dirservers that don't
                                * support BEGIN_DIR, when possible. */
                                * support BEGIN_DIR, when possible. */
+  int PortForwarding; /**< If true, use NAT-PMP or UPnP to automatically
+                       * forward the DirPort and ORPort on the NAT device */
+  char *PortForwardingHelper; /** < Filename or full path of the port
+                                  forwarding helper executable */
   int AllowNonRFC953Hostnames; /**< If true, we allow connections to hostnames
   int AllowNonRFC953Hostnames; /**< If true, we allow connections to hostnames
                                 * with weird characters. */
                                 * with weird characters. */
   /** If true, we try resolving hostnames with weird characters. */
   /** If true, we try resolving hostnames with weird characters. */

+ 41 - 0
src/test/test_util.c

@@ -6,6 +6,7 @@
 #include "orconfig.h"
 #include "orconfig.h"
 #define CONTROL_PRIVATE
 #define CONTROL_PRIVATE
 #define MEMPOOL_PRIVATE
 #define MEMPOOL_PRIVATE
+#define UTIL_PRIVATE
 #include "or.h"
 #include "or.h"
 #include "config.h"
 #include "config.h"
 #include "control.h"
 #include "control.h"
@@ -1208,6 +1209,45 @@ test_util_load_win_lib(void *ptr)
 }
 }
 #endif
 #endif
 
 
+static void
+clear_hex_errno(char *hex_errno)
+{
+  memset(hex_errno, ' ', HEX_ERRNO_SIZE - 2);
+  hex_errno[HEX_ERRNO_SIZE - 1] = '\n';
+  hex_errno[HEX_ERRNO_SIZE] = '\0';
+}
+
+static void
+test_util_exit_status(void *ptr)
+{
+  char hex_errno[HEX_ERRNO_SIZE + 1];
+
+  (void)ptr;
+
+  clear_hex_errno(hex_errno);
+  format_helper_exit_status(0, 0, hex_errno);
+  tt_str_op(hex_errno, ==, "         0/0\n");
+
+  clear_hex_errno(hex_errno);
+  format_helper_exit_status(0, 0x7FFFFFFF, hex_errno);
+  tt_str_op(hex_errno, ==, "  0/7FFFFFFF\n");
+
+  clear_hex_errno(hex_errno);
+  format_helper_exit_status(0xFF, -0x80000000, hex_errno);
+  tt_str_op(hex_errno, ==, "FF/-80000000\n");
+
+  clear_hex_errno(hex_errno);
+  format_helper_exit_status(0x7F, 0, hex_errno);
+  tt_str_op(hex_errno, ==, "        7F/0\n");
+
+  clear_hex_errno(hex_errno);
+  format_helper_exit_status(0x08, -0x242, hex_errno);
+  tt_str_op(hex_errno, ==, "      8/-242\n");
+
+ done:
+  ;
+}
+
 #define UTIL_LEGACY(name)                                               \
 #define UTIL_LEGACY(name)                                               \
   { #name, legacy_test_helper, 0, &legacy_setup, test_util_ ## name }
   { #name, legacy_test_helper, 0, &legacy_setup, test_util_ ## name }
 
 
@@ -1234,6 +1274,7 @@ struct testcase_t util_tests[] = {
 #ifdef MS_WINDOWS
 #ifdef MS_WINDOWS
   UTIL_TEST(load_win_lib, 0),
   UTIL_TEST(load_win_lib, 0),
 #endif
 #endif
+  UTIL_TEST(exit_status, 0),
   END_OF_TESTCASES
   END_OF_TESTCASES
 };
 };
 
 

+ 1 - 1
src/tools/tor-fw-helper/tor-fw-helper.c

@@ -115,7 +115,7 @@ log_commandline_options(int argc, char **argv)
   logfile = fopen("tor-fw-helper.log", "a");
   logfile = fopen("tor-fw-helper.log", "a");
   if (NULL == logfile)
   if (NULL == logfile)
     return -1;
     return -1;
- 
+
   /* Send all commandline arguments to the file */
   /* Send all commandline arguments to the file */
   now = time(NULL);
   now = time(NULL);
   retval = fprintf(logfile, "START: %s\n", ctime(&now));
   retval = fprintf(logfile, "START: %s\n", ctime(&now));