6
votes

I have a TCP server that uses Boost ASIO. I've noticed that when using link scoped IPv6 addresses on Linux I can't create a boost::asio::ip::tcp::acceptor without throwing an exception. Using a Global IPv6 address or an IPv4 address will work fine.

I'm pretty sure the problem is with the scope id not being set correctly but I can't figure out how to fix the problem.

I'm developing on Ubuntu 11.04 LTS using the ubuntu provided boost 1.40.0 library. Here's a very dumbed down version of the server code I have that shows the problem:

#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/bind.hpp>
#include <iostream>
#include <string>

/* To Compile:
g++ -Wall -o ./asio-ipv6 ./asio-ipv6.cpp -lboost_system 
*/

typedef boost::shared_ptr<boost::asio::ip::tcp::socket>   TcpSocketPtr;
typedef boost::shared_ptr<boost::asio::ip::tcp::acceptor> TcpAcceptorPtr;
typedef boost::shared_ptr<boost::asio::ip::tcp::endpoint> TcpEndpointPtr;

class AsioServer{
public:
  AsioServer(boost::asio::io_service& io): io_(io){};

  //throws
  void accept(const std::string& ipString,unsigned short port){
    boost::asio::ip::address addr = boost::asio::ip::address::from_string(ipString);
    std::cout << "Valid IP address " << ipString << std::endl;

    this->endpoint_.reset(new boost::asio::ip::tcp::endpoint(addr,port));
    std::cout << "Created endpoint" << std::endl;

    //Will throw if a link local IPv6 address is used
    acceptor_.reset(new boost::asio::ip::tcp::acceptor(this->io_,*(this->endpoint_)));

    std::cout << "About to accept on " << *(this->endpoint_) << std::endl;
    this->socket_.reset(new boost::asio::ip::tcp::socket(this->io_));
    this->acceptor_->async_accept(*socket_ ,boost::bind(&AsioServer::handle_accept,this,boost::asio::placeholders::error)); 
  }

private:
  boost::asio::io_service& io_;
  TcpSocketPtr socket_;
  TcpAcceptorPtr acceptor_;
  TcpEndpointPtr endpoint_;

  void handle_accept(const boost::system::error_code &ec){
    if(!ec){
      std::cout << "Accepted connection!" << std::endl;
    }
    else{
      std::cout << "Error accepting connection" << std::endl;
    }
  };

};

int main(int argc, char* argv[]){

  boost::asio::io_service io;

  std::string ipString("0.0.0.0");
  if(argc > 1){
    ipString.assign(argv[1]);   
  }
  std::cout << "IP Set to " << ipString << std::endl;

  AsioServer server(io);

  try{
    server.accept(ipString,4444);
  }
  catch(const std::exception& e){
    std::cout << "Error caught:  " << e.what() << std::endl;
  }

  io.run();

  std::cout << "Done!" << std::endl;

  return 0;
}

On this machine I have the following configured for eth0:

$ ifconfig eth0
eth0      Link encap:Ethernet  HWaddr 00:0c:29:10:cf:0e  
          inet addr:192.168.97.162  Bcast:192.168.97.255  Mask:255.255.255.0
          inet6 addr: 2620:1c:8000:190:20c:29ff:fe10:cf0e/64 Scope:Global
          inet6 addr: fe80::20c:29ff:fe10:cf0e/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:112470185 errors:2 dropped:1 overruns:0 frame:0
          TX packets:5900249 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:2187237130 (2.1 GB)  TX bytes:1640094885 (1.6 GB)
          Interrupt:19 Base address:0x2000 

Running the program using 192.168.97.162, 0.0.0.0, 127.0.0.1, ::1 or 2620:1c:8000:190:20c:29ff:fe10:cf0e works fine but using fe80::20c:29ff:fe10:cf0e fails to create an acceptor with an "Invalid Argument" exception being thrown.

$ ./asio-ipv6 fe80::20c:29ff:fe10:cf0e
IP Set to fe80::20c:29ff:fe10:cf0e
Valid IP address fe80::20c:29ff:fe10:cf0e
Created endpoint
Error caught:  Invalid argument
Done!

This reminds me of using ping6 and seeing the same "Invalid Argument" error. The fix is to pass the the interface appended to the IPv6 address.

$ ping6 fe80::219:b9ff:fe2b:3a53  #Won't work
connect: Invalid argument
$ ping6 fe80::219:b9ff:fe2b:3a53%eth0 #Pass the interface to use to ping6 
PING fe80::219:b9ff:fe2b:3a53%eth0(fe80::219:b9ff:fe2b:3a53) 56 data bytes

This doesn't seem to work for Boost Asio when I try to create the IP address from a string.

$ ./asio-ipv6 fe80::20c:29ff:fe10:cf0e%eth0
IP Set to fe80::20c:29ff:fe10:cf0e%eth0
Error caught:  Invalid argument
Done!

My question is how do you go about listening on a link scoped IPv6 address using Boost ASIO? The boost::asio::ip::address_v6 class has a scope_id() member function but I'm not sure where to get the scope id as an unsigned long or if this is even the problem.

3

3 Answers

5
votes

This can be accomplished using a boost::asio::ip::tcp::resolver

--- ipv6.cc.orig        2012-04-24 12:03:02.349911481 -0500
+++ ipv6.cc     2012-04-24 12:02:07.053037095 -0500
@@ -10,42 +10,67 @@

 typedef boost::shared_ptr<boost::asio::ip::tcp::socket>   TcpSocketPtr;
 typedef boost::shared_ptr<boost::asio::ip::tcp::acceptor> TcpAcceptorPtr;
+typedef boost::shared_ptr<boost::asio::ip::tcp::resolver> TcpResolverPtr;
 typedef boost::shared_ptr<boost::asio::ip::tcp::endpoint> TcpEndpointPtr;

 class AsioServer{
 public:
   AsioServer(boost::asio::io_service& io): io_(io){};

-  //throws
-  void accept(const std::string& ipString,unsigned short port){
-    boost::asio::ip::address addr = boost::asio::ip::address::from_string(ipString);
-    std::cout << "Valid IP address " << ipString << std::endl;
-
-    this->endpoint_.reset(new boost::asio::ip::tcp::endpoint(addr,port));
-    std::cout << "Created endpoint" << std::endl;
-
-    //Will throw if a link local IPv6 address is used
-    acceptor_.reset(new boost::asio::ip::tcp::acceptor(this->io_,*(this->endpoint_)));
-
-    std::cout << "About to accept on " << *(this->endpoint_) << std::endl;
-    this->socket_.reset(new boost::asio::ip::tcp::socket(this->io_));
-    this->acceptor_->async_accept(*socket_ ,boost::bind(&AsioServer::handle_accept,this,boost::asio::placeholders::error)); 
+  void resolve(const std::string& ipString, const std::string& service){
+    this->resolver_.reset(new boost::asio::ip::tcp::resolver(io_));
+    boost::asio::ip::tcp::resolver::query query( ipString, service );
+    this->resolver_->async_resolve( query, boost::bind( &AsioServer::handle_resolve, this, boost::asio::placeholders::error, boost::asio::placeholders::iterator ) );
   }
-
 private:
   boost::asio::io_service& io_;
   TcpSocketPtr socket_;
   TcpAcceptorPtr acceptor_;
   TcpEndpointPtr endpoint_;
+  TcpResolverPtr resolver_;
+
+  void accept(const boost::asio::ip::tcp::resolver::iterator iterator) {
+    endpoint_.reset(new boost::asio::ip::tcp::endpoint(iterator->endpoint()));
+    //Will throw if a link local IPv6 address is used
+    acceptor_.reset(new boost::asio::ip::tcp::acceptor(this->io_));
+    acceptor_->open(this->endpoint_->protocol());
+    acceptor_->bind(*(this->endpoint_));
+    acceptor_->listen();
+
+
+    std::cout << "About to accept on " << *(this->endpoint_) << std::endl;
+    socket_.reset(new boost::asio::ip::tcp::socket(this->io_));
+    acceptor_->async_accept(*socket_ ,boost::bind(&AsioServer::handle_accept,this,boost::asio::placeholders::error)); 
+  }
+

   void handle_accept(const boost::system::error_code &ec){
     if(!ec){
       std::cout << "Accepted connection!" << std::endl;
     }
     else{
-      std::cout << "Error accepting connection" << std::endl;
+      std::cout << "Error accepting connection: " << boost::system::system_error(ec).what() << std::endl;
+    }
+  }
+  
+  void handle_resolve(
+        const boost::system::error_code& ec,
+        boost::asio::ip::tcp::resolver::iterator iterator
+       )
+  {
+    if(ec) {
+      std::cerr << "could not resolve: " << boost::system::system_error(ec).what() << std::endl;
+      return;
     }
-  };
+
+    if ( iterator == boost::asio::ip::tcp::resolver::iterator() ) {
+      std::cerr << "no endpoints resolved" << std::endl;
+      return;
+    }
+
+    this->accept(iterator);
+
+  }

 };

@@ -62,7 +87,7 @@
   AsioServer server(io);

   try{
-    server.accept(ipString,4444);
+    server.resolve(ipString,"4444");
   }
   catch(const std::exception& e){
     std::cout << "Error caught:  " << e.what() << std::endl;

sample session

[samm@t410 Desktop]$ ifconfig | grep fe80
          inet6 addr: fe80::5a94:6bff:fe7c:e760/64 Scope:Link
[samm@t410 Desktop]$ g++ -g ipv6.cc -lboost_system -lboost_thread-mt
[samm@t410 Desktop]$ ./a.out fe80::5a94:6bff:fe7c:e760%wlan0
IP Set to fe80::5a94:6bff:fe7c:e760%wlan0
About to accept on [fe80::5a94:6bff:fe7c:e760%wlan0]:4444
^C
[samm@t410 Desktop]$
1
votes

Use ip link ls from the command line or if_nametoindex() in your code to get the interface index for a given device. E.g. on my laptop:

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN qlen 1000
    link/ether f0:de:f1:5c:39:37 brd ff:ff:ff:ff:ff:ff

In my case the interface index for eth0 is 2. Use this as the scope id if Boost only accepts a numerical value.

1
votes

This is a simpler solution using the spawn function.

#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/bind.hpp>
#include <iostream>
#include <string>

class AsioServer{
public:
    AsioServer(boost::asio::io_service& io): m_io(io) {}

    void accept(const std::string& ip_address, const std::string& service) {
        boost::asio::spawn(m_io, [this, ip_address, service](boost::asio::yield_context yield) {
            try {
                using tcp = boost::asio::ip::tcp;
                boost::system::error_code ec;
                tcp::resolver resolver(m_io);
                tcp::resolver::iterator iterator =
                        resolver.async_resolve(tcp::resolver::query(ip_address, service), yield);
                if (iterator == tcp::resolver::iterator()) {
                    std::cerr << "No endpoints resolved for "<< ip_address  << std::endl;
                    return;
                }
                tcp::endpoint endpoint(iterator->endpoint());
                std::cout << "Endpoint: " << endpoint << std::endl;
                tcp::acceptor acceptor(m_io);
                acceptor.open(endpoint.protocol());
                acceptor.bind(endpoint);
                acceptor.listen();
                std::cout << "About to accept on " << endpoint << std::endl;
                for (;;)
                {
                  tcp::socket socket(m_io);
                  acceptor.async_accept(socket, yield[ec]);
                  if (ec) {
                      std::cerr << "Error accepting connection: " << boost::system::system_error(ec).what() << std::endl;
                  } else {
                      std::cout << "Accepted connection from " << socket.remote_endpoint() << std::endl;
                      //std::make_shared<session>(std::move(socket))->go();
                  }
                }
            }
            catch( std::exception &e) {
                std::cerr << "Error setting up accepting port: " << e.what() << std::endl;
            }
          });
    }

private:
    boost::asio::io_service& m_io;
};

int main(int argc, char* argv[])
{
    boost::asio::io_service io;
    std::string ipString("0.0.0.0");

    if(argc > 1){
      ipString.assign(argv[1]);
    }
    std::cout << "IP Set to " << ipString << std::endl;

    AsioServer server(io);

    try{
      server.accept(ipString,"4444");
    }
    catch(const std::exception& e){
      std::cout << "Error caught:  " << e.what() << std::endl;
    }

    io.run();
    std::cout << "Done!" << std::endl;
    return 0;
}

Execution result

AsioServer$ ifconfig | grep fe80
          adr inet6: fe80::be5f:f4ff:fef7:8a46/64 Scope:Lien
AsioServer$ ./a_out fe80::be5f:f4ff:fef7:8a46%eth0
IP Set to fe80::be5f:f4ff:fef7:8a46%eth0
Endpoint: [fe80::be5f:f4ff:fef7:8a46%eth0]:4444
About to accept on [fe80::be5f:f4ff:fef7:8a46%eth0]:4444