c++boost-asio

modifying UDP perl code to modern boost, errors in packet


I have found some old code in perl that will create a packet to be sent off to a program:

the original code.

The relevant part is:

my $discovery_socket = IO::Socket::INET->new(
  Proto => 'udp',
  LocalAddr => '0.0.0.0',
  LocalPort => 3040,
) or die "Can't bind udp/3040: $!";

my $discovery_io = IO::Async::Socket->new(
  handle => $discovery_socket,
  on_recv => sub {
    my ($self, $dgram, $addr) = @_;
    my ($err, $hostname, $portname) = Socket::getnameinfo($addr);
    my (undef, undef, $seq) = unpack "ccna6xx", $dgram;
    print "Received identify with seq $seq from $hostname/$portname\n";
    my $header = pack "ccna6xx",
      0x5a, # magic
      1, # identify reply
      $seq,
      "\x00\x24\x1d\x8b\x0e\x50";
    my $identify = pack "nccccccC4C4C4a4a4a4a32",
      1, 1, 1, 1, # uptime
      1, # mode
      0, # alert
      4, # ip type
      192, 168, 1, 3, # ip addr
      255, 255, 255, 0, # netmask
      192, 168, 1, 1, # gateway
      "\x01\x02\x03\x04", # app ver
      "\x01\x02\x03\x04", # boot ver
      "\x01\x02\x03\x04", # app ver
      "PL ETH/USB"; # name

    send $discovery_socket, $header . $identify, 0, $addr;
  },
  on_recv_error => sub {
    my ($self, $errno) = @_;
    die "recv error $errno on discovery";
  }
);

I've attempted to recreate this with the following function in c++, (plus other surrounding code that probably isn't relevant:

void handle_receive(const boost::system::error_code& error,
                    std::size_t bytes_transferred) {
    if (!error || error == boost::asio::error::message_size) {
        std::string hostname = remote_endpoint_.address().to_string();
        unsigned short port = remote_endpoint_.port();
        std::cout << "Received identify from " << hostname << "/" << port << std::endl;
        
        uint16_t seq;
        std::memcpy(&seq, recv_buffer_.data() + 2, sizeof(seq));
        
        // Construct header
        std::array<char, 64> header;
        header[0] = 0x5A; // magic
        header[1] = 1;    // identify reply
        std::memcpy(header.data() + 2, &seq, sizeof(seq));
        std::memcpy(header.data() + 4, "\x00\x24\x1d\x8b\x0e\x50", 6); // eth_addr
        
        // Construct identify reply
        std::array<char, 64> identify_reply;
        std::memset(identify_reply.data(), 0, 4); // uptime
        identify_reply[4] = 1; // mode
        identify_reply[5] = 0; // alert
        identify_reply[6] = 4; // ip type
        std::memcpy(identify_reply.data() + 7, "\x0a\x0a\x0a\x0a", 4); // ip addr: 10.10.10.10
        std::memcpy(identify_reply.data() + 11, "\xff\xff\xff\x00", 4); // netmask
        std::memcpy(identify_reply.data() + 15, "\x0a\x0a\x0a\x01", 4); // gateway
        std::memcpy(identify_reply.data() + 19, "\x01\x02\x03\x04", 4); // app ver
        std::memcpy(identify_reply.data() + 23, "\x01\x02\x03\x04", 4); // boot ver
        std::memcpy(identify_reply.data() + 27, "\x01\x02\x03\x04", 4); // hw ver
        std::memcpy(identify_reply.data() + 31, "PL ETH/USB", 10); // name
        
        // Convert header and identify reply to vector of chars
        std::vector<char> send_buffer(header.begin(), header.end());
        send_buffer.insert(send_buffer.end(), identify_reply.begin(), identify_reply.end());
        // Convert packet to vector of strings
        std::vector<std::string> packetStrings = convertPacketToStrings(header, identify_reply,false);
        
        // Print each string to console
        for (const auto& str : packetStrings) {
            std::cout << str << std::endl;
        }
        // Send packet
        socket_.send_to(boost::asio::buffer(send_buffer), remote_endpoint_);
        
        // Start receiving again
        start_receive();
    }
}

The program that send a udp packet to this server code will get a discovery packet as a response, which the program does infact see as follows:

response

But the IP is not correct, it seems to be some randomly generated sequence of numbers that show up at byte position 21, in the following hex -> decimal sequence, which I highlighted in bold.

90 1 81 79 0 36 29 139 14 80 61 4 1 0 0 0 92 251 245 181 1 128 62 228 104 46 166 107 1 0 0 0 72 46 166 107 1 0 0 0 0 0 0 0 1 0 0 0 200 95 59 4 1 0 0 0 16 192 76 1 0 96 0 0
0 0 0 0 1 0 4 10 10 10 10 255 255 255 0 10 10 10 1 1 2 3 4 1 2 3 4 1 2 3 4 80 76 32 69 84 72 47 85 83 66 67 224 74 1 0 0 0 72 69 224 74 1 0 0 0 208 0 0 0 0 0 0 0

I'm sure the structure or byte order is wrong somehow, but I don't see it.

Any ideas?


Solution

  • Correctly Interpreting The Perl

    Looking at the perl docs, n indicates packing a 16bit unsigned big-endian integer:

    n An unsigned short (16-bit) in "network" (big-endian) order.

    That implies that pack "nccccccC4C4C4a4a4a4a32" puts the parts at these offsets:

    my $identify = pack "nccccccC4C4C4a4a4a4a32",
    1, 1, 1, 1,         # uptime n c c c, offset 0
    1,                  # mode c,         offset 5
    0,                  # alert c,        offset 6
    4,                  # ip type c,      offset 7
    192, 168, 1, 3,     # ip addr,        offset 8
    255, 255, 255, 0,   # netmask,        offset 12
    192, 168, 1, 1,     # gateway,        offset 16
    "\x01\x02\x03\x04", # app ver,        offset 20
    "\x01\x02\x03\x04", # boot ver,       offset 24
    "\x01\x02\x03\x04", # app ver,        offset 28
    "PL ETH/USB";       # name,           offset 32
    

    I have confirmed this with the simplified perl script:

    #!/usr/bin/perl
    use strict;
    use warnings;
    
    # my (undef, undef, $seq) = unpack "ccna6xx", $dgram;
    my $seq = 1;
    
    print "Received identify with seq $seq\n";
    
    my $header = pack "ccna6xx",
      0x5a, # magic
      1, # identify reply
      $seq,
      "\x00\x24\x1d\x8b\x0e\x50"; # mac addr of the device (00:24:1d:8b:0e:50)
    
    my $identify = pack "nccccccC4C4C4a4a4a4a32",
      1, 1, 1, 1, # uptime
      1, # mode
      0, # alert
      4, # ip type
      192, 168, 1, 3, # ip addr
      255, 255, 255, 0, # netmask
      192, 168, 1, 1, # gateway
      "\x01\x02\x03\x04", # app ver
      "\x01\x02\x03\x04", # boot ver
      "\x01\x02\x03\x04", # app ver
      "PL ETH/USB"; # name
    
    # print both header and identify as hex arrays to stdout
    print join " ", map { sprintf "%02x", $_ } unpack "C*", $header;
    print "\n";
    print join " ", map { sprintf "%02x", $_ } unpack "C*", $identify;
    print "\n";
    

    Which outputs Live On Coliru:

    # header
    5a 01 00 01 00 24 1d 8b 0e 50 00 00
    
    # identify reply
    00 01 01 01 01 01 00 04 c0 a8 01 03 ff ff ff 00 c0 a8 01 01 01 02 03 04 01 02 03 04 01 02 03 04 50 4c 20 45 54 48 2f 55 53 42 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    

    So that confirms my counts (see netmask start at offset #12).

    Bugs In The C++

    Regardless, this doesn't do the same thing:

    std::vector<char> send_buffer(header.begin(), header.end());
    

    That adds all header bytes, which is 64 bytes. The perl version only has 12 header bytes. I would probably mimic it like so:

    // Construct header
    std::vector<uint8_t> header{
        0x5A,                                   // magic
        1,                                      // identify reply
        seq[0], seq[1],                         // seq, still big endian
        0x00,   0x24,   0x1d, 0x8b, 0x0e, 0x50, // eth_addr 00:24:1d:8b:0e:50
        0x00,   0x00                            // trailing xx
    };
    assert(header.size() == 12);
    

    Now the identify reply could be something like:

    // Construct identify reply
    std::array<uint8_t, 64> identify_reply{
        0, 1, 0, 1, 0, 0, 0, 4, // uptime, mode, alert, ip type
        192, 168, 1, 3,          // ip addr
        255, 255, 255, 0,        // netmask
        192, 168, 1, 1,          // gateway
        1, 2, 3, 4,              // app ver
        1, 2, 3, 4,              // boot ver
        1, 2, 3, 4,              // hw ver
        'P', 'L', ' ', 'E', 'T', 'H', '/', 'U', 'S', 'B' // name
        // 22 (64-42) trailing NUL bytes for free 
    };
    

    To deal with the network ordering, you could do e.g.:

    uint16_t uptime = ::htons(1);
    std::memcpy(identify_reply.data() + 0, &uptime, 2);
    

    Or, vice versa:

    uint8_t rawseq[2]; // big endian
    std::memcpy(&rawseq, recv_buffer_.data() + 2, sizeof(rawseq));
    
    {
        uint16_t seq;
        std::memcpy(&seq, recv_buffer_.data() + 2, sizeof(seq));
        seq = ntohs(seq);
    }
    

    Live C++ Demo

    Live On Coliru

    #include <array>
    #include <cassert>
    #include <fmt/ranges.h>
    #include <netinet/in.h>
    #include <vector>
    
    void simplified_handle_receive() {
        std::vector<uint8_t> recv_buffer_{0x5A, 0x01, 0x00, 0x01, 0x00, 0x24,
                                          0x1d, 0x8b, 0x0e, 0x50, 0x00, 0x00}; // no idea here, really
    
        uint8_t rawseq[2];
        std::memcpy(&rawseq, recv_buffer_.data() + 2, sizeof(rawseq));
    
        {
            uint16_t seq;
            std::memcpy(&seq, recv_buffer_.data() + 2, sizeof(seq));
            seq = ntohs(seq);
        }
    
        // Construct header
        std::vector<uint8_t> buffer{
            0x5A,                                         // magic
            1,                                            // identify reply
            rawseq[0], rawseq[1],                         // seq, still big endian
            0x00,      0x24,      0x1d, 0x8b, 0x0e, 0x50, // eth_addr 00:24:1d:8b:0e:50
            0x00,      0x00                               // trailing xx
        };
        assert(buffer.size() == 12);
    
        // Construct identify reply
        std::array<uint8_t, 64> identify_reply{
            0,   1,   0,   1,   0,   0,   0,   4, // uptime, mode, alert, ip type
            192, 168, 1,   3,                     // ip addr
            255, 255, 255, 0,                     // netmask
            192, 168, 1,   1,                     // gateway
            1,   2,   3,   4,                     // app ver
            1,   2,   3,   4,                     // boot ver
            1,   2,   3,   4,                     // hw ver
            'P', 'L', ' ', 'E', 'T', 'H', '/', 'U',
            'S', 'B' // name
            // 22 (64-42) trailing NUL bytes for free
        };
    
    
        fmt::print("Header:         {::02x}\n", buffer);
        fmt::print("Identify reply: {::02x}\n", identify_reply);
    
        buffer.insert(buffer.end(), identify_reply.begin(), identify_reply.end());
        fmt::print("Header + Identify reply: {::02x}\n", buffer);
    };
    
    int main() {
        simplified_handle_receive();
    }
    

    Printing

    Header:         [5a, 01, 00, 01, 00, 24, 1d, 8b, 0e, 50, 00, 00]
    Identify reply: [00, 01, 00, 01, 00, 00, 00, 04, c0, a8, 01, 03, ff, ff, ff, 00, c0, a8, 01, 01, 01, 02, 03, 04, 01, 02, 03, 04, 01, 02, 03, 04, 50, 4c, 20, 45, 54, 48, 2f, 55, 53, 42, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00]
    Header + Identify reply: [5a, 01, 00, 01, 00, 24, 1d, 8b, 0e, 50, 00, 00, 00, 01, 00, 01, 00, 00, 00, 04, c0, a8, 01, 03, ff, ff, ff, 00, c0, a8, 01, 01, 01, 02, 03, 04, 01, 02, 03, 04, 01, 02, 03, 04, 50, 4c, 20, 45, 54, 48, 2f, 55, 53, 42, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00]