Subject: [local-bind] local-bind: Support binding to local interface/IPs.

[local-bind] local-bind: Support binding to local interface/IPs.

From: Ben Greear <greearb_at_candelatech.com>
Date: Tue, 25 May 2010 12:35:22 -0700

This allows users to specify which interface outbound
requests will originate from on multi-homed machines.

Also support setting list of servers as a comma-separated
string.

Signed-off-by: Ben Greear <greearb_at_candelatech.com>

---
:100644 100644 368c73a... 3b4ff13... M	ares.h
:100644 100644 1f561aa... d5b768c... M	ares_init.c
:100644 100644 d00368a... b43fdff... M	ares_options.c
:100644 100644 0df5cb7... 6f86a3a... M	ares_private.h
:100644 100644 3ef1ddb... e416132... M	ares_process.c
 ares.h         |   19 +++++++++
 ares_init.c    |   24 +++++++++++
 ares_options.c |  124 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 ares_private.h |    7 +++
 ares_process.c |   38 +++++++++++++++--
 5 files changed, 208 insertions(+), 4 deletions(-)
diff --git a/ares.h b/ares.h
index 368c73a..3b4ff13 100644
--- a/ares.h
+++ b/ares.h
@@ -313,6 +313,20 @@ CARES_EXTERN void ares_destroy(ares_channel channel);
 
 CARES_EXTERN void ares_cancel(ares_channel channel);
 
+/* These next 3 configure local binding for the out-going socket
+ * connection.  Use these to specify source IP and/or network device
+ * on multi-homed systems.
+ */
+CARES_EXTERN void ares_set_local_ip4(ares_channel channel, uint32_t local_ip);
+
+/* local_ip6 should be 16 bytes in length */
+CARES_EXTERN void ares_set_local_ip6(ares_channel channel,
+                                     const unsigned char* local_ip6);
+
+/* local_dev_name should be null terminated. */
+CARES_EXTERN void ares_set_local_dev(ares_channel channel,
+                                     const char* local_dev_name);
+
 CARES_EXTERN void ares_set_socket_callback(ares_channel channel,
                                            ares_sock_create_callback callback,
                                            void *user_data);
@@ -496,6 +510,7 @@ CARES_EXTERN void ares_free_data(void *dataptr);
 
 CARES_EXTERN const char *ares_strerror(int code);
 
+/* TODO:  Hold port here as well. */
 struct ares_addr_node {
   struct ares_addr_node *next;
   int family;
@@ -508,6 +523,10 @@ struct ares_addr_node {
 CARES_EXTERN int ares_set_servers(ares_channel channel,
                                   struct ares_addr_node *servers);
 
+/* Incomming string format: host[:port][,host[:port]]... */
+CARES_EXTERN int ares_set_servers_csv(ares_channel channel,
+                                      const char* servers);
+
 CARES_EXTERN int ares_get_servers(ares_channel channel,
                                   struct ares_addr_node **servers);
 
diff --git a/ares_init.c b/ares_init.c
index 1f561aa..d5b768c 100644
--- a/ares_init.c
+++ b/ares_init.c
@@ -168,6 +168,10 @@ int ares_init_options(ares_channel *channelptr, struct ares_options *options,
   channel->last_server = 0;
   channel->last_timeout_processed = (time_t)now.tv_sec;
 
+  memset(&channel->local_dev_name, 0, sizeof(channel->local_dev_name));
+  channel->local_ip4 = 0;
+  memset(&channel->local_ip6, 0, sizeof(channel->local_ip6));
+  
   /* Initialize our lists of queries */
   ares__init_list_head(&(channel->all_queries));
   for (i = 0; i < ARES_QID_TABLE_SIZE; i++)
@@ -1594,6 +1598,26 @@ unsigned short ares__generate_new_id(rc4_key* key)
   return r;
 }
 
+void ares_set_local_ip4(ares_channel channel, uint32_t local_ip)
+{
+  channel->local_ip4 = local_ip;
+}
+
+/* local_ip6 should be 16 bytes in length */
+void ares_set_local_ip6(ares_channel channel,
+                        const unsigned char* local_ip6) {
+  memcpy(&channel->local_ip6, local_ip6, sizeof(channel->local_ip6));
+}
+
+/* local_dev_name should be null terminated. */
+void ares_set_local_dev(ares_channel channel,
+                        const char* local_dev_name) {
+  strncpy(channel->local_dev_name, local_dev_name,
+          sizeof(channel->local_dev_name));
+  channel->local_dev_name[sizeof(channel->local_dev_name) - 1] = 0;
+}
+
+
 void ares_set_socket_callback(ares_channel channel,
                               ares_sock_create_callback cb,
                               void *data)
diff --git a/ares_options.c b/ares_options.c
index d00368a..b43fdff 100644
--- a/ares_options.c
+++ b/ares_options.c
@@ -125,3 +125,127 @@ int ares_set_servers(ares_channel channel,
 
   return ARES_SUCCESS;
 }
+
+/* Incomming string format: host[:port][,host[:port]]... */
+int ares_set_servers_csv(ares_channel channel,
+                         const char* _csv)
+{
+  struct ares_addr_node *srvr;
+  int num_srvrs = 0;
+  int i;
+  char* csv = NULL;
+  char* ptr;
+  char* start_host;
+  int port;
+  bool found_port;
+  int rv = ARES_SUCCESS;
+  struct ares_addr_node *servers = NULL;
+  struct ares_addr_node *last = NULL;
+  
+  if (ares_library_initialized() != ARES_SUCCESS)
+    return ARES_ENOTINITIALIZED;
+
+  if (!channel)
+    return ARES_ENODATA;
+
+  ares__destroy_servers_state(channel);
+
+  i = strlen(_csv);
+  if (i == 0)
+     return ARES_SUCCESS; /* blank all servers */
+  
+  csv = malloc(i + 2);
+  strcpy(csv, _csv);
+  if (csv[i-1] != ',') { /* make parsing easier by ensuring ending ',' */
+    csv[i] = ',';
+    csv[i+1] = 0;
+  }
+      
+  ptr = csv;
+  start_host = csv;
+  found_port = false;
+  for (ptr; *ptr; ptr++) {
+    if (*ptr == ',') {
+      char* pp = ptr - 1;
+      struct in_addr in4;
+      struct in6_addr in6;
+      struct ares_addr_node *s = NULL;
+        
+      *ptr = 0; /* null terminate host:port string */
+      /* Got an entry..see if port was specified. */
+      while (pp > start_host) {
+        if (*pp == ':')
+          break; /* yes */
+        if (!isdigit(*pp)) {
+          /* Found end of digits before we found :, so wasn't a port */
+          pp = ptr;
+          break;
+        }
+        pp--;
+      }
+      if ((pp != start_host) && ((pp + 1) < ptr)) {
+        /* Found it. */
+        found_port = true;
+        port = atoi(pp + 1);
+        *pp = 0; /* null terminate host */
+      }
+      /* resolve host, try ipv4 first, rslt is in network byte order */
+      rv = inet_pton(AF_INET, start_host, &in4);
+      if (!rv) {
+        /* Ok, try IPv6 then */
+        rv = inet_pton(AF_INET6, start_host, &in6);
+        if (!rv) {
+          rv = ARES_EBADSTR;
+          goto out;
+        }
+        /* was ipv6, add new server */
+        s = malloc(sizeof(*s));
+        if (!s) {
+          rv = ARES_ENOMEM;
+          goto out;
+        }
+        s->family = AF_INET6;
+        memcpy(&s->addr, &in6, sizeof(struct ares_in6_addr));
+      }
+      else {
+        /* was ipv4, add new server */
+        s = malloc(sizeof(*s));
+        if (!s) {
+          rv = ARES_ENOMEM;
+          goto out;
+        }
+        s->family = AF_INET;
+        memcpy(&s->addr, &in4, sizeof(struct in_addr));           
+      }
+      if (s) {
+        /* TODO:  Add port to ares_addr_node and assign it here. */
+        
+        s->next = NULL;
+        if (last) {
+          last->next = s;
+        }
+        else {
+          servers = s;
+          last = s;
+        }
+      }
+
+      /* Set up for next one */
+      found_port = false;
+      start_host = ptr + 1;
+    }
+  }
+
+  rv = ares_set_servers(channel, servers);
+  
+  out:
+  if (csv)
+    free(csv);
+  while (servers) {
+    struct ares_addr_node *s = servers;
+    servers = servers->next;
+    free(s);
+  }
+  
+  return rv;
+}
diff --git a/ares_private.h b/ares_private.h
index 0df5cb7..6f86a3a 100644
--- a/ares_private.h
+++ b/ares_private.h
@@ -257,6 +257,13 @@ struct ares_channeldata {
   int nsort;
   char *lookups;
 
+  /* For binding to local devices and/or IP addresses.  Leave
+   * them null/zero for no binding.
+   */
+  char local_dev_name[32];
+  uint32_t local_ip4;
+  unsigned char local_ip6[16];
+
   int optmask; /* the option bitfield passed in at init time */
 
   /* Server addresses and communications state */
diff --git a/ares_process.c b/ares_process.c
index 3ef1ddb..e416132 100644
--- a/ares_process.c
+++ b/ares_process.c
@@ -91,7 +91,6 @@ static void skip_server(ares_channel channel, struct query *query,
                         int whichserver);
 static void next_server(ares_channel channel, struct query *query,
                         struct timeval *now);
-static int configure_socket(ares_socket_t s, ares_channel channel);
 static int open_tcp_socket(ares_channel channel, struct server_state *server);
 static int open_udp_socket(ares_channel channel, struct server_state *server);
 static int same_questions(const unsigned char *qbuf, int qlen,
@@ -869,7 +868,7 @@ static int setsocknonblock(ares_socket_t sockfd,    /* operate on this */
 #endif
 }
 
-static int configure_socket(ares_socket_t s, ares_channel channel)
+static int configure_socket(ares_socket_t s, int family, ares_channel channel)
 {
   setsocknonblock(s, TRUE);
 
@@ -892,6 +891,37 @@ static int configure_socket(ares_socket_t s, ares_channel channel)
                  sizeof(channel->socket_receive_buffer_size)) == -1)
     return -1;
 
+#ifdef SO_BINDTODEVICE
+  if (channel->local_dev_name[0]) {
+    if (setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE,
+                   channel->local_dev_name, sizeof(channel->local_dev_name))) {
+      /* Only root can do this, and usually not fatal if it doesn't work, so */
+      /* just continue on. */
+    }
+  }
+#endif
+  
+  if (family == AF_INET) {
+    if (channel->local_ip4) {
+      struct sockaddr_in sa;
+      memset(&sa, 0, sizeof(sa));
+      sa.sin_family = AF_INET;
+      sa.sin_addr.s_addr = htonl(channel->local_ip4);
+      if (bind(s, (struct sockaddr*)&sa, sizeof(sa)) < 0)
+        return -1;
+    }
+  }
+  else if (family == AF_INET6) {
+    if (memcmp(channel->local_ip6, &in6addr_any, sizeof(channel->local_ip6)) != 0) {
+      struct sockaddr_in6 sa;
+      memset(&sa, 0, sizeof(sa));
+      sa.sin6_family = AF_INET6;
+      memcpy(&sa.sin6_addr, channel->local_ip6, sizeof(channel->local_ip6));
+      if (bind(s, (struct sockaddr*)&sa, sizeof(sa)) < 0)
+        return -1;
+    }
+  }
+  
   return 0;
  }
 
@@ -936,7 +966,7 @@ static int open_tcp_socket(ares_channel channel, struct server_state *server)
     return -1;
 
   /* Configure it. */
-  if (configure_socket(s, channel) < 0)
+  if (configure_socket(s, server->addr.family, channel) < 0)
     {
        sclose(s);
        return -1;
@@ -1028,7 +1058,7 @@ static int open_udp_socket(ares_channel channel, struct server_state *server)
     return -1;
 
   /* Set the socket non-blocking. */
-  if (configure_socket(s, channel) < 0)
+  if (configure_socket(s, server->addr.family, channel) < 0)
     {
        sclose(s);
        return -1;
-- 
1.6.2.5
Received on 2010-05-25