9

I am trying to receive a UDP packet using Boost asio. My code is based off of this blocking UDP client example from the asio documentation.

I am trying to receive a BOOTP-like UDP packet from a C6655 TI DSP, which are being transmitted in 3 second intervals. I have Wireshark watching the same interface my program is listening on, and it can see the packets arriving (see below for the exact packet data, exported from Wireshark). The packets aren't really coming from the DSP, I captured one with tcpdump and I'm simulating it from a Raspberry Pi with packeth.

However, my program does not receive the packets. It has a 4 second timeout (since the DSP broadcasts every 3 seconds). If it hits the timeout, it prints a message to that effect, otherwise it's supposed to print the number of bytes received. The full (compilable) source code of the program follows (about 100 lines).

The command is being invoked with the parameters 192.168.5.122 67 4000, which means listen on 192.168.5.122:67 with a 4000 millisecond timeout.

Edit: In addition to the code below, I also tried this as my endpoint: udp::endpoint listen_endpoint(boost::asio::ip::address_v4::any(), atoi(argv[2])); as well as the IP address 0.0.0.0 as suggested by a search result somewhere.

I also added the following to no avail:

boost::asio::socket_base::broadcast option(true);
socket_.set_option(option);

I do have a program that is able to properly receive this packet, written using Berkeley sockets. It's not doing anything special that I can see, beyond binding to INADDR_ANY.

Here is the complete program:

//
// blocking_udp_client.cpp
// ~~~~~~~~~~~~~~~~~~~~~~~
//
#include <boost/asio/deadline_timer.hpp>
#include <boost/asio/io_service.hpp>
#include <boost/asio/ip/udp.hpp>
#include <boost/bind.hpp>
#include <boost/date_time/posix_time/posix_time_types.hpp>
#include <iostream>

using boost::asio::deadline_timer;
using boost::asio::ip::udp;

class listener
{
public:
    listener(const udp::endpoint& listen_endpoint)
        : socket_(io_service_, listen_endpoint)
        , deadline_(io_service_)
    {
        deadline_.expires_at(boost::posix_time::pos_infin);
        check_deadline();
    }

    std::size_t receive(const boost::asio::mutable_buffer& buffer, boost::posix_time::time_duration timeout, boost::system::error_code& ec)
    {
        deadline_.expires_from_now(timeout);
        ec = boost::asio::error::would_block;
        std::size_t length = 0;
        socket_.async_receive(boost::asio::buffer(buffer), boost::bind(&listener::handle_receive, _1, _2, &ec, &length));

        // TODO: The following do/while is hinky. Does run_one() need to happen before the comparison?
        do io_service_.run_one();
        while (ec == boost::asio::error::would_block);

        return length;
    }

private:
    void check_deadline()
    {
        if (deadline_.expires_at() <= deadline_timer::traits_type::now())
        {
            // cancel() won't work on XP. Something about using close() instead... Look it up. I'm doing this on Win10.
            socket_.cancel();
            deadline_.expires_at(boost::posix_time::pos_infin);
        }
        deadline_.async_wait(boost::bind(&listener::check_deadline, this));
    }

    static void handle_receive(const boost::system::error_code& ec, std::size_t length, boost::system::error_code* out_ec, std::size_t* out_length)
    {
        *out_ec = ec;
        *out_length = length;
    }

private:
    boost::asio::io_service io_service_;
    udp::socket socket_;
    deadline_timer deadline_;
};

int main(int argc, char* argv[])
{
    try
    {
        if (argc != 4)
        {
            std::cerr << "Usage: blocking_udp_timeout <listen_addr> <listen_port> <timeout_ms>\n";
            return 1;
        }

        udp::endpoint listen_endpoint(boost::asio::ip::address::from_string("0.0.0.0"), atoi(argv[2]));
        std::cout << "Endpoint: " << listen_endpoint << std::endl;

        auto timeout = atoi(argv[3]);
        std::cout << "Timeout : " << timeout << std::endl;

        listener c(listen_endpoint);

        for (;;)
        {
            char data[1024];
            boost::system::error_code ec;
            auto n = c.receive(boost::asio::buffer(data), boost::posix_time::milliseconds{timeout}, ec);

            if (ec)
            {
                std::cout << "Receive error: " << ec.message() << "\n";
            }
            else
            {
                std::cout << "Received " << n << " bytes." << std::endl;
            }
        }
    }
    catch (std::exception& e)
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }

    return 0;
}

Here is the packet that I'm trying to receive. This includes the Ethernet frame:

0000   ff ff ff ff ff ff c4 ed ba aa 28 35 08 00 45 00  ..........(5..E.
0010   01 48 00 01 00 00 10 11 a9 a5 00 00 00 00 00 00  .H..............
0020   00 00 00 44 00 43 01 34 00 00 01 01 06 00 12 34  ...D.C.4.......4
0030   56 78 00 01 00 00 00 00 00 00 00 00 00 00 00 00  Vx..............
0040   00 00 00 00 00 00 c4 ed ba aa 28 35 00 00 00 00  ..........(5....
0050   00 00 00 00 00 00 74 69 2d 62 6f 6f 74 2d 74 61  ......ti-boot-ta
0060   62 6c 65 2d 73 76 72 00 00 00 00 00 00 00 00 00  ble-svr.........
0070   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0080   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0090   00 00 00 00 00 00 74 69 2d 62 6f 6f 74 2d 74 61  ......ti-boot-ta
00a0   62 6c 65 2d 30 30 30 37 00 00 00 00 00 00 00 00  ble-0007........
00b0   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00c0   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00d0   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00e0   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00f0   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0100   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0110   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0120   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0130   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0140   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0150   00 00 00 00 00 00                                ......

I do have a Berkeley socket implementation that can receive this packet (I have removed error handling and other misc. code):

{
    struct sockaddr_in servaddr;
    socklen_t len;
    char mesg[RECV_BUFFER_LENGTH];

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(67);
    bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    n = recvfrom(sockfd, mesg, RECV_BUFFER_LENGTH, 0, NULL, &len);
}
Steve
  • 6,334
  • 4
  • 39
  • 67
  • BOOTP is normally a broadcast, which requires special flags to receive – Ben Voigt Oct 03 '15 at 01:18
  • @BenVoigt Can you explain how to accomplish this via `boost::asio`? – Steve Oct 03 '15 at 02:02
  • I have tried listening on port `0` (which seems strange) as was suggested by some other search results I've found, but to no avail. – Steve Oct 03 '15 at 02:07
  • Not port 0, but address `INADDR_ANY` can sometimes help. Mostly, though, you need `setsockopt()` using the `SO_BROADCAST` option. Don't know what the equivalent function in `asio` is. – Ben Voigt Oct 03 '15 at 04:10
  • The documentation I can find about `SO_BROADCAST` says `Permits sending of broadcast messages`, but doesn't mention receive. I did try `boost::asio::ip::address_v4::any()` as the endpoint to no avail. – Steve Oct 03 '15 at 11:31
  • 1
    The client code looks fine: `socket_base::broadcast` is only required when sending UDP broadcast messages, and binding to the interface address on which data is expected or `address_v4::any()` (i.e. `0.0.0.0`) is fine. Here is a simple [demo](http://coliru.stacked-crooked.com/a/ef569d1ca70dd24b). At a cursory glance, the IPv4 destination address of `0.0.0.0` in the posted ethernet frame is suspicious, as `0.0.0.0` often has special meaning in routing and does not look as though it is a broadcast address. – Tanner Sansbury Oct 05 '15 at 17:41
  • 1
    @TannerSansbury The documentation from TI states that "The Ethernet-ready announcement frame is made in the form of a BOOTP request so it can use a standard format. No response is processed for this message and it is constructed so that most—if not all—BOOTP and DHCP servers will discard it." This might be what you're referring to (they're not that specific). However, what would you expect a BOOTP client to put in that field, when they don't have an IP address (or know where to get one)? – Steve Oct 05 '15 at 18:41
  • @Steve I think the initial problem being observed is a routing problem, in layers below the BOOTP-like protocol. The posted ethernet frame has a BOOTP-like message using UDP (layer4) as its transport, and the IPv4 destination address of `0.0.0.0` in the IPv4 header (layer 3). In the [UDP broadcast RFC](http://tools.ietf.org/html/rfc919), a destination address of `0.0.0.0` is unspecified. The [BOOTP RFC](https://tools.ietf.org/html/rfc951) even specifies that the server address is unknown, the IP header's destination should be set to `255.255.255.255`. – Tanner Sansbury Oct 05 '15 at 20:47
  • 1
    I'll try editing the packet and changing that IP. That would be unfortunate, though, since the packets comes from a boot ROM I have no control over. However, we have a program written on Linux that runs on an ARM, and the ARM is the only thing on a tiny on-board Ethernet network with that DSP. It's able to receive this packet with the regular Berkeley socket interface (and that source doesn't seem to be doing anything more special than binding to INADDR_ANY). When I run my asio program on this same device, it does not receive the packet. This would rule out a lower-layer issue, wouldn't it? – Steve Oct 06 '15 at 12:40
  • 1
    @Steve Unless the kernel documentation states it supports the _old-style broadcast address_ of `0.0.0.0`, then I would not depend on it as being a guarantee (although you may have no choice). Do you observe the same behavior when using non-connected reads, such as [`socket.async_receive_from()`](http://www.boost.org/doc/libs/1_59_0/doc/html/boost_asio/reference/basic_datagram_socket/async_receive_from.html) ([demo](http://coliru.stacked-crooked.com/a/ae4b87381182c76b))? Connected-read operations would be permitted to filter out the _invalid_ `0.0.0.0` source address. – Tanner Sansbury Oct 06 '15 at 13:32
  • Can you tell us what arguments the function `socket()` is called with in your Berkeley sockets implementation, and in your Boost implementation? Also, have you looked at [Boost's mechanisms to support other protocols](http://www.boost.org/doc/libs/1_59_0/doc/html/boost_asio/overview/networking/other_protocols.html)? – Iwillnotexist Idonotexist Oct 06 '15 at 17:25
  • @TannerSansbury I have yet to try your demo with my code, but I plan to soon. – Steve Oct 06 '15 at 18:07
  • @IwillnotexistIdonotexist I added the sockets implementation to the end of the question. – Steve Oct 06 '15 at 18:07
  • Just to check the obvious: Have you double checked the correct port number? At the first glance, it looks like it is OK. Source and Destination IP address are both set to 0.0.0.0. Maybe your IP Stack drops the packet. Have you tried with a very simple implementation directly based on the socket API? – cdonat Oct 08 '15 at 13:54
  • @cdonat Yes, I've a working BSD sockets implementation that can receive the packet. See the code near the bottom of the question. – Steve Oct 08 '15 at 14:36
  • @Steve are you `root` on your desktop? IIRC, [only root can bind to ports <1024](http://stackoverflow.com/questions/413807/is-there-a-way-for-non-root-processes-to-bind-to-privileged-ports-1024-on-l), and it happens you're trying to bind to 67. Are you also sure nothing else is binding to that port? And that your firewall is disabled or at least is permitting access to that port? – Iwillnotexist Idonotexist Oct 08 '15 at 23:05
  • @IwillnotexistIdonotexist I'm running the program using the same account as the BSD sockets version that works, on the same machine, same packets, all that. – Steve Oct 09 '15 at 00:57
  • That BSD Sockets version is not at all equivalent! For starters, you don't have any timeout or loop in it. – Lightness Races in Orbit Oct 12 '15 at 08:59
  • @Steve To the best of my knowledge, Boost.Asio is making the equivalent `socket()` and `bind()` calls. The only notable difference is that `async_receive()` and `async_receive_from()` are implemented in terms of `recvmsg()` instead of `recvfrom()`. Does the Boost.Asio socket ever indicate data is available via [`socket.available()`](http://www.boost.org/doc/libs/1_59_0/doc/html/boost_asio/reference/basic_datagram_socket/available/overload1.html)? – Tanner Sansbury Oct 12 '15 at 15:53

1 Answers1

3

Consider what happens after socket_.cancel(), but before the next call to socket_.async_receive(). If any data arrives, there is no "receive handler" assigned to the socket. When the timer expires, it causes run_once() to be called 3 times (because each async_wait(), expires_at() and some of the others cause cancellations of already-assigned handlers and cause the already-assigned handlers to be posted to the run queue with the error code operation_aborted).

You haven't mentioned what your timeout is set to. The example on which you base your code (the one from Boost documentation) sets the timeout to 10 seconds, but you make it configurable. If the timeout is too tight, this would cause a spin (post->cancel previous->call previous handler->post->etc.) and possibly call socket_.cancel() while receiving the packet. If that's the case, you wouldn't have to go so far as to test it with a broadcast. You'd be able to see it with a point-to-point connection, too.

Edit: while using a long timeout (4000ms) and your exact code, I was able to receive a sent broadcast. I had to install "traditional" netcat because BSD netcat is broken. But the line below worked:

echo "hello world" | nc.traditional -b -u 192.168.XXX.255 1500

XXX.255 is not literally "XXX.255". It's my local broadcast address. -b option in nc.bsd is broken (you can see why if you strace nc.bsd with above options).

unix stackexchange nc.traditional has a good example of how others figured out why nc.bsd cannot do UDP broadcasts (-b option does nothing).

PackEth wasn't able to to send broadcasts or p-t-p traffic, so I wouldn't use it as a measure of whether you can send broadcast traffic. Admittedly, I don't have much experience with it, so I don't know if it's broken or if I don't have it configured right.

Community
  • 1
  • 1
Dmitry Rubanovich
  • 2,471
  • 19
  • 27
  • I do not think that type of looping is possible, even in the presence of a tight or instant timeout. If data arrives and there is no pending read operation, then the datagram will still be queued within the kernel. If data is available, then a read will occur within the initiating `async_receive()` operation, causing the completion handler to be posted with success, at which point the read operation is no longer cancelable. – Tanner Sansbury Oct 12 '15 at 14:07
  • @Tanner Sansbury, you are right. I've tried a number of scenarios and they all work point to point and fail with the broadcast address. I have tried setting the broadcast option before and after bind. Nothing seems to get to work. I am doing this on Linux Mint with boost 49, btw. Thought I'd mention this because the only thing that everyone seems to agree on is that broadcast routing works different on Linux and Windows. Oh, and I switched it to async_receive_from(). DIdn't help. Although it could be a PackETH issue. I don't see packets arriving when sent p-t-p while nc -u works. – Dmitry Rubanovich Oct 12 '15 at 20:27