From 92dca5eff016ff850e00993ebc2f4ff31dd484c4 Mon Sep 17 00:00:00 2001
From: Damien Ciabrini <dciabrin@redhat.com>
Date: Wed, 8 Feb 2017 11:19:39 +0000
Subject: [PATCH] Backport Mysql WL#3126 TCP address binding for mysql client
 library

- This patch extends the MySQL C API making it possible to
  select which local interface to use when clients connect
  to a MySQL Server using TCP. The user calls
  'mysql_options' to specify the address which will will be used in
  'mysql_real_connect' to extract a list of address info's which
  the source socket will 'bind' to. If no matching source+destination
  is found an error will be returned.

- Since the client_ip memeber of st_mysql_options is only used by
  embedded server and the bind_address is only used by non embedded server
  we can reuse it and thus avoid breaking the ABI(like was done in MySQL
  Cluster) without using the "extension".

  Example:
    mysql_options(mysql, MYSQL_OPT_BIND, "192.168.10.12");

- Add support for --bind-address to the mysql, mysqladmin,
  mysqlbinlog, mysqlcheck, mysqldump, mysqlimport
  and mysqlshow programs
---
 client/mysql.cc       |  6 ++++
 client/mysqladmin.cc  |  6 ++++
 client/mysqlbinlog.cc |  6 ++++
 client/mysqlcheck.c   |  6 ++++
 client/mysqldump.c    |  6 ++++
 client/mysqlimport.c  |  6 ++++
 client/mysqlshow.c    |  6 ++++
 include/mysql.h       | 15 ++++++++-
 include/mysql.h.pp    |  5 ++-
 sql-common/client.c   | 86 +++++++++++++++++++++++++++++++++++++++++++++++++--
 10 files changed, 144 insertions(+), 4 deletions(-)

diff --git a/client/mysql.cc b/client/mysql.cc
index 136713e..20c5fb1 100644
--- a/client/mysql.cc
+++ b/client/mysql.cc
@@ -158,6 +158,7 @@ static void my_vidattr(chtype attrs)
 static uint verbose=0,opt_silent=0,opt_mysql_port=0, opt_local_infile=0;
 static uint my_end_arg;
 static char * opt_mysql_unix_port=0;
+static char *opt_bind_addr = NULL;
 static int connect_flag=CLIENT_INTERACTIVE;
 static my_bool opt_binary_mode= FALSE;
 static int interrupted_query= 0;
@@ -1492,6 +1493,9 @@ sig_handler window_resize(int sig)
   {"batch", 'B',
    "Don't use history file. Disable interactive behavior. (Enables --silent.)",
    0, 0, 0, GET_NO_ARG, NO_ARG, 0, 0, 0, 0, 0, 0},
+  {"bind-address", 0, "IP address to bind to.",
+   (uchar**) &opt_bind_addr, (uchar**) &opt_bind_addr, 0, GET_STR,
+   REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
   {"character-sets-dir", OPT_CHARSETS_DIR,
    "Directory for character set files.", &charsets_dir,
    &charsets_dir, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
@@ -4618,6 +4622,8 @@ char *mysql_authentication_dialog_ask(MYSQL *mysql, int type,
     mysql_options(&mysql,MYSQL_OPT_CONNECT_TIMEOUT,
 		  (char*) &timeout);
   }
+  if (opt_bind_addr)
+    mysql_options(&mysql, MYSQL_OPT_BIND, opt_bind_addr);
   if (opt_compress)
     mysql_options(&mysql,MYSQL_OPT_COMPRESS,NullS);
   if (using_opt_local_infile)
diff --git a/client/mysqladmin.cc b/client/mysqladmin.cc
index 9642903..5df3244 100644
--- a/client/mysqladmin.cc
+++ b/client/mysqladmin.cc
@@ -43,6 +43,7 @@
 static my_bool debug_info_flag= 0, debug_check_flag= 0;
 static uint tcp_port = 0, option_wait = 0, option_silent=0, nr_iterations;
 static uint opt_count_iterations= 0, my_end_arg;
+static char *opt_bind_addr = NULL;
 static ulong opt_connect_timeout, opt_shutdown_timeout;
 static char * unix_port=0;
 static char *opt_plugin_dir= 0, *opt_default_auth= 0;
@@ -128,6 +129,9 @@ enum commands {
 
 static struct my_option my_long_options[] =
 {
+  {"bind-address", 0, "IP address to bind to.",
+   (uchar**) &opt_bind_addr, (uchar**) &opt_bind_addr, 0, GET_STR,
+   REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
   {"count", 'c',
    "Number of iterations to make. This works with -i (--sleep) only.",
    &nr_iterations, &nr_iterations, 0, GET_UINT,
@@ -335,6 +339,8 @@ int main(int argc,char *argv[])
 
   sf_leaking_memory=0; /* from now on we cleanup properly */
 
+  if (opt_bind_addr)
+    mysql_options(&mysql,MYSQL_OPT_BIND,opt_bind_addr);
   if (opt_compress)
     mysql_options(&mysql,MYSQL_OPT_COMPRESS,NullS);
   if (opt_connect_timeout)
diff --git a/client/mysqlbinlog.cc b/client/mysqlbinlog.cc
index e27a4a8..eab3cd1 100644
--- a/client/mysqlbinlog.cc
+++ b/client/mysqlbinlog.cc
@@ -114,6 +114,7 @@
 #endif
 static char* user = 0;
 static char* pass = 0;
+static char *opt_bind_addr = NULL;
 static char *charset= 0;
 
 static uint verbose= 0;
@@ -1309,6 +1310,9 @@ Exit_status process_event(PRINT_EVENT_INFO *print_event_info, Log_event *ev,
    "--base64-output[=name] option is given at all, the default is 'auto'.",
    &opt_base64_output_mode_str, &opt_base64_output_mode_str,
    0, GET_STR, OPT_ARG, 0, 0, 0, 0, 0, 0},
+  {"bind-address", 0, "IP address to bind to.",
+   (uchar**) &opt_bind_addr, (uchar**) &opt_bind_addr, 0, GET_STR,
+   REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
   /*
     mysqlbinlog needs charsets knowledge, to be able to convert a charset
     number found in binlog to a charset name (to be able to print things
@@ -1799,6 +1803,8 @@ static Exit_status safe_connect()
 
   if (opt_protocol)
     mysql_options(mysql, MYSQL_OPT_PROTOCOL, (char*) &opt_protocol);
+  if (opt_bind_addr)
+    mysql_options(mysql, MYSQL_OPT_BIND, opt_bind_addr);
 #ifdef HAVE_SMEM
   if (shared_memory_base_name)
     mysql_options(mysql, MYSQL_SHARED_MEMORY_BASE_NAME,
diff --git a/client/mysqlcheck.c b/client/mysqlcheck.c
index 3a6f546..9c6fee3 100644
--- a/client/mysqlcheck.c
+++ b/client/mysqlcheck.c
@@ -57,6 +57,7 @@
 DYNAMIC_ARRAY views4repair;
 static char *shared_memory_base_name=0;
 static uint opt_protocol=0;
+static char *opt_bind_addr = NULL;
 
 enum operations { DO_CHECK=1, DO_REPAIR, DO_ANALYZE, DO_OPTIMIZE, DO_FIX_NAMES };
 const char *operation_name[]=
@@ -86,6 +87,9 @@
    "If a checked table is corrupted, automatically fix it. Repairing will be done after all tables have been checked, if corrupted ones were found.",
    &opt_auto_repair, &opt_auto_repair, 0, GET_BOOL, NO_ARG, 0,
    0, 0, 0, 0, 0},
+  {"bind-address", 0, "IP address to bind to.",
+   (uchar**) &opt_bind_addr, (uchar**) &opt_bind_addr, 0, GET_STR,
+   REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
   {"character-sets-dir", OPT_CHARSETS_DIR,
    "Directory for character set files.", (char**) &charsets_dir,
    (char**) &charsets_dir, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
@@ -1092,6 +1096,8 @@ static int dbConnect(char *host, char *user, char *passwd)
 #endif
   if (opt_protocol)
     mysql_options(&mysql_connection,MYSQL_OPT_PROTOCOL,(char*)&opt_protocol);
+  if (opt_bind_addr)
+    mysql_options(&mysql_connection, MYSQL_OPT_BIND, opt_bind_addr);
   if (shared_memory_base_name)
     mysql_options(&mysql_connection,MYSQL_SHARED_MEMORY_BASE_NAME,shared_memory_base_name);
 
diff --git a/client/mysqldump.c b/client/mysqldump.c
index ced65f7..50a5467 100644
--- a/client/mysqldump.c
+++ b/client/mysqldump.c
@@ -140,6 +140,7 @@ static ulong find_set(TYPELIB *lib, const char *x, size_t length,
 static uint opt_use_gtid;
 static uint my_end_arg;
 static char * opt_mysql_unix_port=0;
+static char *opt_bind_addr = NULL;
 static int   first_error=0;
 /*
   multi_source is 0 if old server or 2 if server that support multi source 
@@ -243,6 +244,9 @@ static void dynstr_append_mem_checked(DYNAMIC_STRING *str, const char *append,
    "Adds 'STOP SLAVE' prior to 'CHANGE MASTER' and 'START SLAVE' to bottom of dump.",
    &opt_slave_apply, &opt_slave_apply, 0, GET_BOOL, NO_ARG,
    0, 0, 0, 0, 0, 0},
+  {"bind-address", 0, "IP address to bind to.",
+   (uchar**) &opt_bind_addr, (uchar**) &opt_bind_addr, 0, GET_STR,
+   REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
   {"character-sets-dir", OPT_CHARSETS_DIR,
    "Directory for character set files.", &charsets_dir,
    &charsets_dir, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
@@ -1684,6 +1688,8 @@ static int connect_to_db(char *host, char *user,char *passwd)
 #endif
   if (opt_protocol)
     mysql_options(&mysql_connection,MYSQL_OPT_PROTOCOL,(char*)&opt_protocol);
+  if (opt_bind_addr)
+    mysql_options(&mysql_connection,MYSQL_OPT_BIND,opt_bind_addr);
 #ifdef HAVE_SMEM
   if (shared_memory_base_name)
     mysql_options(&mysql_connection,MYSQL_SHARED_MEMORY_BASE_NAME,shared_memory_base_name);
diff --git a/client/mysqlimport.c b/client/mysqlimport.c
index aee445d..d7f126a 100644
--- a/client/mysqlimport.c
+++ b/client/mysqlimport.c
@@ -58,6 +58,7 @@ static char *add_load_option(char *ptr,const char *object,
 		*escaped=0, *opt_columns=0, 
 		*default_charset= (char*) MYSQL_AUTODETECT_CHARSET_NAME;
 static uint     opt_mysql_port= 0, opt_protocol= 0;
+static char *opt_bind_addr = NULL;
 static char * opt_mysql_unix_port=0;
 static char *opt_plugin_dir= 0, *opt_default_auth= 0;
 static longlong opt_ignore_lines= -1;
@@ -71,6 +72,9 @@ static char *add_load_option(char *ptr,const char *object,
 
 static struct my_option my_long_options[] =
 {
+  {"bind-address", 0, "IP address to bind to.",
+   (uchar**) &opt_bind_addr, (uchar**) &opt_bind_addr, 0, GET_STR,
+   REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
   {"character-sets-dir", OPT_CHARSETS_DIR,
    "Directory for character set files.", (char**) &charsets_dir,
    (char**) &charsets_dir, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
@@ -453,6 +457,8 @@ static MYSQL *db_connect(char *host, char *database,
 #endif
   if (opt_protocol)
     mysql_options(mysql,MYSQL_OPT_PROTOCOL,(char*)&opt_protocol);
+  if (opt_bind_addr)
+    mysql_options(mysql,MYSQL_OPT_BIND,opt_bind_addr);
 #ifdef HAVE_SMEM
   if (shared_memory_base_name)
     mysql_options(mysql,MYSQL_SHARED_MEMORY_BASE_NAME,shared_memory_base_name);
diff --git a/client/mysqlshow.c b/client/mysqlshow.c
index eec4a8d..4874d1e 100644
--- a/client/mysqlshow.c
+++ b/client/mysqlshow.c
@@ -43,6 +43,7 @@
 static char *shared_memory_base_name=0;
 #endif
 static uint opt_protocol=0;
+static char *opt_bind_addr = NULL;
 
 static void get_options(int *argc,char ***argv);
 static uint opt_mysql_port=0;
@@ -131,6 +132,8 @@ int main(int argc, char **argv)
 #endif
   if (opt_protocol)
     mysql_options(&mysql,MYSQL_OPT_PROTOCOL,(char*)&opt_protocol);
+  if (opt_bind_addr)
+    mysql_options(&mysql,MYSQL_OPT_BIND,opt_bind_addr);
 #ifdef HAVE_SMEM
   if (shared_memory_base_name)
     mysql_options(&mysql,MYSQL_SHARED_MEMORY_BASE_NAME,shared_memory_base_name);
@@ -187,6 +190,9 @@ int main(int argc, char **argv)
 
 static struct my_option my_long_options[] =
 {
+  {"bind-address", 0, "IP address to bind to.",
+   (uchar**) &opt_bind_addr, (uchar**) &opt_bind_addr, 0, GET_STR,
+   REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
   {"character-sets-dir", 'c', "Directory for character set files.",
    (char**) &charsets_dir, (char**) &charsets_dir, 0, GET_STR, REQUIRED_ARG, 0,
    0, 0, 0, 0, 0},
diff --git a/include/mysql.h b/include/mysql.h
index f088ad6..9622d0d 100644
--- a/include/mysql.h
+++ b/include/mysql.h
@@ -213,7 +213,20 @@ struct st_mysql_options {
   my_bool unused3;
   my_bool unused4;
   enum mysql_option methods_to_use;
-  char *client_ip;
+  union {
+    /*
+      The ip/hostname to use when authenticating
+      client against embedded server built with
+      grant tables - only used in embedded server
+    */
+    char *client_ip;
+
+    /*
+      The local address to bind when connecting to
+      remote server - not used in embedded server
+    */
+    char *bind_address;
+  };
   /* Refuse client connecting to server if it uses old (pre-4.1.1) protocol */
   my_bool secure_auth;
   /* 0 - never report, 1 - always report (default) */
diff --git a/include/mysql.h.pp b/include/mysql.h.pp
index 32d87b7..35098e8 100644
--- a/include/mysql.h.pp
+++ b/include/mysql.h.pp
@@ -293,7 +293,10 @@ struct st_mysql_options {
   my_bool unused3;
   my_bool unused4;
   enum mysql_option methods_to_use;
-  char *client_ip;
+  union {
+    char *client_ip;
+    char *bind_address;
+  };
   my_bool secure_auth;
   my_bool report_data_truncation;
   int (*local_infile_init)(void **, const char *, void *);
diff --git a/sql-common/client.c b/sql-common/client.c
index 756d1c1..b63d644 100644
--- a/sql-common/client.c
+++ b/sql-common/client.c
@@ -1258,6 +1258,10 @@ void mysql_read_default_options(struct st_mysql_options *options,
         case OPT_report_data_truncation:
           options->report_data_truncation= opt_arg ? MY_TEST(atoi(opt_arg)) : 1;
           break;
+        case OPT_bind_address:
+          my_free(options->bind_address, MYF(MY_ALLOW_ZERO_PTR));
+          options->bind_address= my_strdup(opt_arg, MYF(MY_WME));
+          break;
         case OPT_plugin_dir:
           {
             char buff[FN_REFLEN], buff2[FN_REFLEN], *buff2_ptr= buff2;
@@ -3288,11 +3292,11 @@ int run_plugin_auth(MYSQL *mysql, char *data, uint data_len,
       (!mysql->options.protocol ||
        mysql->options.protocol == MYSQL_PROTOCOL_TCP))
   {
-    struct addrinfo *res_lst, hints, *t_res;
+    struct addrinfo *res_lst, *client_bind_ai_lst= NULL, hints, *t_res;
     int gai_errno;
     char port_buf[NI_MAXSERV];
     my_socket sock= SOCKET_ERROR;
-    int saved_error= 0, status= -1;
+    int saved_error= 0, status= -1, bind_results= 0;
 
     unix_socket=0;				/* This is not used */
 
@@ -3328,9 +3332,35 @@ int run_plugin_auth(MYSQL *mysql, char *data, uint data_len,
       goto error;
     }
 
+    /* Get address info for client bind name if it is provided */
+    if (mysql->options.bind_address)
+    {
+      int bind_gai_errno= 0;
+
+      DBUG_PRINT("info",("Resolving addresses for client bind: '%s'",
+                         mysql->options.bind_address));
+      /* Lookup address info for name */
+      bind_gai_errno= getaddrinfo(mysql->options.bind_address, 0,
+                                  &hints, &client_bind_ai_lst);
+      if (bind_gai_errno)
+      {
+        DBUG_PRINT("info",("client bind getaddrinfo error %d", bind_gai_errno));
+        set_mysql_extended_error(mysql, CR_UNKNOWN_HOST, unknown_sqlstate,
+                                 ER(CR_UNKNOWN_HOST),
+                                 mysql->options.bind_address,
+                                 bind_gai_errno);
+
+        freeaddrinfo(res_lst);
+        goto error;
+      }
+      DBUG_PRINT("info", ("  got address info for client bind name"));
+    }
+
     /*
       A hostname might map to multiple IP addresses (IPv4/IPv6). Go over the
       list of IP addresses until a successful connection can be established.
+      For each IP address, attempt to bind the socket to each client address
+      for the client-side bind hostname until the bind is successful.
     */
     DBUG_PRINT("info", ("Try connect on all addresses for host."));
     for (t_res= res_lst; t_res; t_res= t_res->ai_next)
@@ -3341,16 +3371,61 @@ int run_plugin_auth(MYSQL *mysql, char *data, uint data_len,
       sock= socket(t_res->ai_family, t_res->ai_socktype, t_res->ai_protocol);
       if (sock == SOCKET_ERROR)
       {
+        DBUG_PRINT("info", ("Socket created was invalid"));
+        /* Try next address if there is one */
         saved_error= socket_errno;
         continue;
       }
 
+      if (client_bind_ai_lst)
+      {
+        struct addrinfo* curr_bind_ai= NULL;
+        DBUG_PRINT("info", ("Attempting to bind socket to bind address(es)"));
+
+        /*
+           We'll attempt to bind to each of the addresses returned, until
+           we find one that works.
+           If none works, we'll try the next destination host address
+           (if any)
+        */
+        curr_bind_ai= client_bind_ai_lst;
+
+        while (curr_bind_ai != NULL)
+        {
+          /* Attempt to bind the socket to the given address */
+          bind_result= bind(sock,
+                            curr_bind_ai->ai_addr,
+                            curr_bind_ai->ai_addrlen);
+          if (!bind_result)
+            break;   /* Success */
+
+          DBUG_PRINT("info", ("bind failed, attempting another bind address"));
+          /* Problem with the bind, move to next address if present */
+          curr_bind_ai= curr_bind_ai->ai_next;
+        }
+
+        if (bind_result)
+        {
+          /*
+            Could not bind to any client-side address with this destination
+             Try the next destination address (if any)
+          */
+          DBUG_PRINT("info", ("All bind attempts with this address failed"));
+          saved_error= socket_errno;
+          closesocket(sock);
+          continue;
+        }
+        DBUG_PRINT("info", ("Successfully bound client side of socket"));
+      }
+
       net->vio= vio_new(sock, VIO_TYPE_TCPIP, VIO_BUFFERED_READ);
       if (!net->vio)
       {
         set_mysql_error(mysql, CR_OUT_OF_MEMORY, unknown_sqlstate);
         closesocket(sock);
         freeaddrinfo(res_lst);
+        if (client_bind_ai_lst)
+          freeaddrinfo(client_bind_ai_lst);
         goto error;
       }
 
@@ -3380,6 +3455,8 @@ int run_plugin_auth(MYSQL *mysql, char *data, uint data_len,
                 sock, status, saved_error));
 
     freeaddrinfo(res_lst);
+    if (client_bind_ai_lst)
+      freeaddrinfo(client_bind_ai_lst);
 
     if (sock == SOCKET_ERROR)
     {
@@ -3798,6 +3875,7 @@ static void mysql_close_free_options(MYSQL *mysql)
   my_free(mysql->options.charset_dir);
   my_free(mysql->options.charset_name);
   my_free(mysql->options.client_ip);
+  /* bind_adress is union with client_ip, already freed above */
   if (mysql->options.init_commands)
   {
     DYNAMIC_ARRAY *init_commands= mysql->options.init_commands;
@@ -4329,6 +4407,10 @@ static MYSQL_RES * cli_use_result(MYSQL *mysql)
   case MYSQL_OPT_RECONNECT:
     mysql->reconnect= *(my_bool *) arg;
     break;
+  case MYSQL_OPT_BIND:
+    my_free(mysql->options.bind_address, MYF(MY_ALLOW_ZERO_PTR));
+    mysql->options.bind_address= my_strdup(arg, MYF(MY_WME));
+    break;
   case MYSQL_OPT_USE_THREAD_SPECIFIC_MEMORY:
     mysql->options.use_thread_specific_memory= *(my_bool *) arg;
     break;
-- 
1.8.3.1

