Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

boost::asio::ip::tcp::socket - How to bind to a specific local port

Tags:

I am making a client socket.

To make things easier for my testers, I'd like to specify the network card and port that the socket will use.

Yesterday, in my Google search, I found: Binding boost asio to local tcp endpoint

By performing the open, bind, and async_connect, I was able to bind to a specific network card and I started seeing traffic in Wireshark.

However, Wireshark reports that the socket has been given a random port rather than the one I specified. I would think if the port was in use it would have filled out the error_code passed to the bind method.

What am I doing wrong?

Here is my minimal example, extracted and edited from my real solution.

// Boost Includes
#include <boost/asio.hpp>
#include <boost/atomic.hpp>
#include <boost/bind.hpp>
#include <boost/thread.hpp>
#include <boost/thread/condition_variable.hpp>

// Standard Includes
#include <exception>
#include <memory>
#include <string>
#include <sstream>


boost::asio::io_service        g_ioService;                     /** ASIO sockets require an io_service to run on*/
boost::thread                  g_thread;                        /** thread that will run the io_service and hence where callbacks are called*/
boost::asio::ip::tcp::socket   g_socket(g_ioService);           /** Aync socket*/
boost::asio::ip::tcp::resolver g_resolver(g_ioService);         /** Resolves IP Addresses*/

//--------------------------------------------------------------------------------------------------
void OnConnect(const boost::system::error_code & errorCode, boost::asio::ip::tcp::resolver::iterator endpoint)
{
    if (errorCode || endpoint == boost::asio::ip::tcp::resolver::iterator())
    {
        // Error - An error occured while attempting to connect
        throw std::runtime_error("An error occured while attempting to connect");
    }

    // We connected to an endpoint

    /*
    // Start reading from the socket
    auto callback = boost::bind(OnReceive, boost::asio::placeholders::error);
    boost::asio::async_read_until(g_socket, m_receiveBuffer, '\n', callback);
    */
}


//--------------------------------------------------------------------------------------------------
void Connect()
{
    const std::string hostName = "10.84.0.36";
    const unsigned int port = 1007;

    // Resolve to translate the server machine name into a list of endpoints
    std::ostringstream converter;
    converter << port;
    const std::string portAsString = converter.str();

    boost::asio::ip::tcp::resolver::query query(hostName, portAsString);

    boost::system::error_code errorCode;
    boost::asio::ip::tcp::resolver::iterator itEnd;
    boost::asio::ip::tcp::resolver::iterator itEndpoint = g_resolver.resolve(query, errorCode);

    if (errorCode || itEndpoint == itEnd)
    {
        // Error - Could not resolve either machine
        throw std::runtime_error("Could not resolve either machine");
    }

    g_socket.open(boost::asio::ip::tcp::v4(), errorCode);
    if (errorCode)
    {
        // Could open the g_socket
        throw std::runtime_error("Could open the g_socket");
    }

    boost::asio::ip::tcp::endpoint localEndpoint(boost::asio::ip::address::from_string("10.86.0.18"), 6000);
    g_socket.bind(localEndpoint, errorCode);
    if (errorCode)
    {
        // Could bind the g_socket to local endpoint
        throw std::runtime_error("Could bind the socket to local endpoint");
    }

    // Attempt to asynchronously connect using each possible end point until we find one that works
    boost::asio::async_connect(g_socket, itEndpoint, boost::bind(OnConnect, boost::asio::placeholders::error, boost::asio::placeholders::iterator));
}

//--------------------------------------------------------------------------------------------------
void g_ioServiceg_threadProc()
{
    try
    {
        // Connect to the server
        Connect();

        // Run the asynchronous callbacks from the g_socket on this thread
        // Until the io_service is stopped from another thread
        g_ioService.run();
    }
    catch (...)
    {
        throw std::runtime_error("unhandled exception caught from io_service g_thread");
    }
}

//--------------------------------------------------------------------------------------------------
int main()
{
    // Start up the IO service thread
    g_thread.swap(boost::thread(g_ioServiceg_threadProc));

    // Hang out awhile
    boost::this_thread::sleep_for(boost::chrono::seconds(60));

    // Stop the io service and allow the g_thread to exit
    // This will cancel any outstanding work on the io_service
    g_ioService.stop();

    // Join our g_thread
    if (g_thread.joinable())
    {
        g_thread.join();
    }

    return true;
}

As you can see in the following screenshot, a random port 32781 was selected rather than my requested port 6000.

Wireshark Screen Shot

like image 364
Christopher Pisz Avatar asked Jun 27 '17 16:06

Christopher Pisz


2 Answers

I doubt topic starter is still interested in this question, but for all of future seekers like myself, here is the solution.

The issue here is that boost::asio::connect closes the socket before calling connect for every endpoint in the provided range:

From boost/asio/impl/connect.hpp:

template <typename Protocol BOOST_ASIO_SVC_TPARAM,
    typename Iterator, typename ConnectCondition> 
Iterator connect(basic_socket<Protocol BOOST_ASIO_SVC_TARG>& s,
    Iterator begin, Iterator end, ConnectCondition connect_condition,
    boost::system::error_code& ec)
{ 
  ec = boost::system::error_code();

  for (Iterator iter = begin; iter != end; ++iter)
  {
    iter = (detail::call_connect_condition(connect_condition, ec, iter, end));
    if (iter != end)
    {
      s.close(ec); // <------
      s.connect(*iter, ec);
      if (!ec)
        return iter;
    }
...
} 

That is why bound address is reset. To keep it bound one can use socket.connect/async_connect(...) directly

like image 59
user3588783 Avatar answered Oct 11 '22 16:10

user3588783


6000 is the remote endpoint port, and it is correctly used (otherwise, you wouldn't be connecting to the server side).

From: https://idea.popcount.org/2014-04-03-bind-before-connect/

A TCP/IP connection is identified by a four element tuple: {source IP, source port, destination IP, destination port}. To establish a TCP/IP connection only a destination IP and port number are needed, the operating system automatically selects source IP and port.

Since you do not bind to a local port, one is selected randomly from the "ephemeral port range". This is, by far, the usual way to connect.

Fear not:

It is possible to ask the kernel to select a specific source IP and port by calling bind() before calling connect()

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Let the source address be 192.168.1.21:1234
s.bind(("192.168.1.21", 1234))
s.connect(("www.google.com", 80))

The sample is python.

You do that, but still get another port. It's likely that the hint port is not available.

Check the information on SO_REUSEADDR and SO_REUSEPORT in the linked article

like image 25
sehe Avatar answered Oct 11 '22 18:10

sehe