Search:

PmWiki

pmwiki.org

edit SideBar

Main / Sockets

General

You don't have to bind a server you are running to a particular interface or IP address. Instead, you can accept connections on any interface.

Endpoint vs Socket: What's the Difference?

In networking, a socket is an IP address and port pair. Strictly technically speaking in core-networking, there is nothing like endpoint, there is only socket. Endpoint is a general term, including pipes, interfaces, nodes and such, while socket is a specific term in networking. A socket is an endpoint per Oracle, but an endpoint is not necessarily a socket.

With the Boost asio library, there are various examples that need further sorting.

In this one, the endpoint is created first, followed by io_service, and both are given to socket, which is then bound to endpoint:

asio::ip::udp::endpoint ep(asio::ip::address_v4::any(), port_num);

asio::io_service ios;

asio::ip::udp::socket sock(ios, ep.protocol());

sock.bind(ep, ec);

In this one, the io_service is first and socket second, and there is no bind:

boost::asio::io_service io_context;

boost::asio::basic_raw_socket<asio::ip::raw> socket_(io_context);

boost::asio::ip::address_v4::bytes_type b = {{127, 0, 0, 1}};

asio::ip::raw::endpoint ep(boost::asio::ip::address_v4(b), 23);

Here's another example from Boost where there is no endpoint even used, and some where it is: https://theboostcpplibraries.com/boost.asio-network-programming. The differences are not adequately explained. Some say that to bind the socket to the interface, an endpoint is required.

Asio resolvers are said to provide endpoint resolution functionality and in other text this is clarified as identifying an endpoint from a name.

This may help clarify. When constructing a basic_raw_socket:

  • Can construct with just execution context only (io_context)
  • Can construct and open with exec context and protocol
  • Can construct and open and bind with exec context and protocol and endpoint

UDP

When you want to use a fixed port for a single connection from your client to a server, you use bind() first to lock in the local port followed by connect(). If you don't bind, then the OS will assign you a random port locally.

Here is some sample code:

struct addrinfo *remote = nullptr;
struct addrinfo hints{0};
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
hints.ai_flags = 0;
int err = getaddrinfo(ipstr.c_str(),portstr.c_str(),&hints,&remote);
if (err != 0) {
printf("getaddrinfo failed with error %d\n",err);
throw std::runtime_error("unable to map IP address:port");

m_socket = socket(remote->ai_family, remote->ai_socktype, remote->ai_protocol);
if (m_socket == INVALID_SOCKET)
  throw std::runtime_error("unable to allocate socket");

// this part is optional - bind to a specific port instead of random
if (m_udp_port != 0) {
  sruct sockaddr_in a{0};
  a.sin_family = AF_INET;
  a.sin_addr.s_addr = INADDR_ANY;
  a.sin_port = htons(m_udp_port);
  int err = bind(m_socket,(struct sockaddr *)&a, sizeof(a));
  if (err)
  printf("Unable to bind UDP socket to port %u, %s\n",m_udp_port,strerror(errno));
}

if (connect(m_socket, remote->ai_addr, remote->ai_addrlen) == -1) {
  int err = errno;
  printf("connect returned error %d\n",err);
  fflush(stdout);
  close(m_socket);
  throw std::runtime_error("unable to connect to UDP server");
}


// Make it non-blocking so we can control timeouts via select() calls
unsigned long val = 1;
if (ioctl (m_socket,FIONBIO,&val) < 0) {
  close(m_socket);
  throw std::runtime_error("unable to set socket to non-blocking");
}

Even though UDP is connectionless, you can still use connect() to set the address of the other side for this socket. Then you can use send() instead of sendto().

Troubleshooting

Don't forget error checking on recv and recvfrom because they can return -1 if the connection is refused or for other issues, whereas they normally give you number of bytes.

Loopback Interface

It's not well publicized, but if you are seeing raw Eth packets received twice on your socket bound to the Linux lo interface it turns out that is expected because they are duplicated:
The loopback interface sends packets down to the bottom and then they come right back up again. Each packet would pass by the network analyzer twice, once on the way down and then again as it gets bounced back up the protocol stack by loopback.

For another application with receiving socket bound to that interface, it will actually read the same packet twice. I believe this is uniquely important when dealing with raw Eth frames (layer 2), and that if the applications are only operating at a higher layer (as is typically the case for single-host client-server testing that commonly takes advantage of the loopback I/F) they wouldn’t see the double packet set because the packet that is “sent down” is no longer applicable to the higher layer.

Why is my socket getting hammered by 42-byte eth frames?

These are likely ARP messages. They originate with the local machine rather than being transmitted, hence why they are allowed to violate the Eth frame minimum size of 60 bytes.

Headers

#include <net/ethernet.h> should be automatically included by the GCC compiler, and is part of the GNU libc. #include <linux/if_ether.h> should be automatically included by the GCC compiler, and is part of the Linux kernel headers in /usr/include/.

In other words, you don't need to point CMake to these locations.

Boost ASIO

There are various examples for setting up a socket, and some use socket.bind(), some use socket.open(), some just put everything in the constructor. socket.bind() is not necessarily the action that opens the socket. Bind is described as being used for the "given local endpoint" whereas connect is for the "specified endpoint".

Don't forget to run as root!

Configuring non-blocking socket

Finally found a good explanation on doing this: https://stackoverflow.com/questions/31939843/boostasioasync-receive-and-0-bytes-in-socket

One key is to catch the error code with a certain error type. If you use a normally blocking call that is set to non-blocking, then it will give you this error and flood your console.

boost::system::error_code ec;
rbytes = m_socket_p->receive( boost::asio::buffer(buf, ETH_FRAME_LEN), 0, ec );

Raw Ethernet Frames

When using a packet socket in Linux using socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL) ), you must provide the layer 2 header when sending. This is defined by struct ether_header in netinet/if_ether.h and includes the destination host, source host, and type. The frame check sequence (CRC) is not included, nor is the preamble, start of frame delimiter, or trailer. These are added by the hardware.

Sample code for the send and receive functions using Boost ASIO library to handle raw Eth frames.

void create_raw_socket_and_send_frame(EthFrame& ethframe)
{       
    sockaddr_ll sockaddr;
    memset(&sockaddr, 0, sizeof(sockaddr));
    sockaddr.sll_family = AF_PACKET;
    sockaddr.sll_protocol = htons(ETH_P_ALL);
    sockaddr.sll_ifindex = if_nametoindex(ethIFname);
    sockaddr.sll_hatype = ARPHRD_ETHER;

    // io_context replaces io_service
    boost::asio::io_context io_context;           
    typedef boost::asio::generic::raw_protocol raw_protocol_t;
    typedef boost::asio::generic::basic_endpoint<raw_protocol_t> raw_endpoint_t;
    raw_protocol_t::socket socket(io_context, raw_protocol_t(AF_PACKET, SOCK_RAW));
    socket.non_blocking();

    std::cout << "Sending frame size: " << ethframe.size() << std::endl;

    try {
        // you must create a Boost buffer to send data via Boost socket
        socket.send_to(boost::asio::buffer(ethframe.data(), ethframe.size()),
                   raw_endpoint_t(&sockaddr, sizeof(sockaddr)) );

	} catch (std::exception& e) {
		std::cerr << "Error: " << e.what() << std::endl;
	}

    socket.close();
}


void create_raw_socket_and_recv_frame(uint8_t* buf, size_t size)
{       
    sockaddr_ll sockaddr;
    memset(&sockaddr, 0, sizeof(sockaddr));
    sockaddr.sll_family = AF_PACKET;
    sockaddr.sll_protocol = htons(ETH_P_ALL);           // only rx packets of this protocol
    sockaddr.sll_ifindex = if_nametoindex(ethIFname); //only rx packets from this IF
    sockaddr.sll_hatype = ARPHRD_ETHER;

    // io_context replaces io_service
    boost::asio::io_context io_context;           
    typedef boost::asio::generic::raw_protocol raw_protocol_t;
    typedef boost::asio::generic::basic_endpoint<raw_protocol_t> raw_endpoint_t;
    raw_protocol_t::socket socket(io_context, raw_protocol_t(AF_PACKET, SOCK_RAW));
    socket.bind(raw_endpoint_t(&sockaddr, sizeof(sockaddr)));   

    std::cout << "Waiting for frame... " << std::endl;

    size_t rbytes;

    try {
        // you must create a Boost buffer for these functions     
        rbytes = socket.receive(boost::asio::buffer(buf, size));


	} catch (std::exception& e) {
		std::cerr << "Error: " << e.what() << std::endl;
	}

    std::cout << "Frame received: " << rbytes << " bytes" << std::endl;

    socket.close();
}

Blocking, Non-Blocking, Asynchronous, Synchronous

While there is some overlapping function implication between the two sets of terms, there are some differences. In the Boost library for example, asynchronous architecture refers to functions that are called by an initiator who has also created a callback function for the asynchronous I/O to call when the procedure is complete. So by definition you would only use non-blocking calls for an asynchronous operation.

Timing

Generally one of three ways are used for interacting with receive capable sockets:

  • Open regular socket, but do not read from it (because read() blocks) until you know there it something to be read. You can use select() or poll() to check whether there are data to read from socket(s), and if there is something, read it, as read() won't block.
  • Switch socket to non-blocking I/O, by setting O_NONBLOCK flag with fcntl() function. In this case read() won't block.
  • Set socket's O_ASYNC flag using FIOASYNC option of ioctl() (see man 7 socket for details). In this case you will receive SIGIO signal when there is something to read from socket.

This last one is considered an asynchronous socket because the program does not do a regular poll action on it.

Receive Socket Binding

If you bind a socket for receiving data to a specific address you can only receive data sent to this specific IP address. For example, if you bind to 127.0.0.1 you will be able to receive data from your own system but not from some other system on the local network, because they cannot send data to your 127.0.0.1: for one any data to 127.0.0.1 will be sent to their own 127.0.0.1 and second your 127.0.0.1 is an address on your internal loopback interface which is not reachable from outside.

You can also bind a socket to a catch-all address like 0.0.0.0 (Ipv4) and :: (Ipv6). In this case it is not bound to a specific IP address but will be able to receive data send to any IP address of the machine.


Page last modified on August 23, 2024, at 07:31 PM