LoginSignup
6
7

More than 5 years have passed since last update.

Boost.Asio で Raw Socket を開くメモ

Last updated at Posted at 2014-11-11

はじめに

リンクレイヤーのパケットを扱いたいときがある、かと言われたらないかもしれませんが……。手探りの覚書として残しておきます。確認環境は clang on Linux です。

概説

C ならばおもむろにソケットを開いて PF_PACKETSOCK_RAW をぶち込めば、低層のパケットを扱うことができますので、C++ でも同様にして開けます。C++ チックになにかできないか探しました。

Boost.Asio ならなんとかしてくれるに違いない。generic::raw_protocolbasic_raw_socket あたりが怪しいのでこの辺りをつついてみます。:
http://www.boost.org/doc/libs/1_57_0/doc/html/boost_asio/reference.html

ちょうど raw_protocol の Examples に見たことのあるような例がありました:

raw_protocol p(AF_INET, IPPROTO_ICMP);

ネイティブのアドレスファミリーを指定しています。ここでソケットとプロトコルのフラグを管理しているようです。C と同様の値を与えれば open 時に raw socket のパラメータを与えることができますね。


順当にいけば次はソケットの bind ですね。ソースを見ると、この辺りは generic::raw_protocol::endpoint が担当しているようです。同様にしてネットワークインターフェースの設定を与えればよさそうですね。

コンストラクタは 4つ ありますが、そのうちの 1つ は void *socket_address と そのサイズを取ります:

結論から言うと socket_addressstruct sockaddr に対応しているので、ここに struct sockaddr_ll をぶち込めば OK です。

struct sockaddr_ll sll;
memset( &sll, 0, sizeof( sll ) );
sll.sll_ifindex = if_nametoindex( "<ネットワークインターフェース名>" );

...::endpoint endpoint( reinterpret_cast< struct sockaddr * >( &sll ), sizeof( sll ) );

あとはイーサネットヘッダを自分で作って書き込んで send_to すれば送信ができます。

送信サンプル

適当な実験用パケットを投げるサンプルは以下です:

#include <netpacket/packet.h>
#include <net/ethernet.h>
#include <net/if.h>

#include <iostream>
#include <iterator>
#include <string>
#include <deque>
#include <stdexcept>
#include <cstdint>
#include <utility>

#include <boost/asio.hpp>
#include <boost/tokenizer.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <boost/range/algorithm/copy.hpp>



/**
 * MAC アドレスを切り分けてイテレータに書き込む
 * @param reverse_byte_order バイトオーダーを反転
 */
template < typename OutputIterator >
OutputIterator tokenize_and_copy(
    std::string const &address,
    OutputIterator     output_iterator,
    bool const         reverse_byte_order = false
)
{
    using SeparatorType = boost::char_separator< char >;
    using TokenizerType = boost::tokenizer< SeparatorType >;

    if ( address.length() != 17 )
    {
        throw std::runtime_error( "the given address is malformed." );
    }

    SeparatorType const separator( ":" );
    TokenizerType const tokenizer( address, separator );

    auto const transform_function = [] ( std::string const &string ) -> int
    {
        return std::stoi( string, nullptr, 16 );
    };
    auto const range = tokenizer |
        boost::adaptors::transformed(
            transform_function
        );

    std::deque< char > result;
    if ( reverse_byte_order )
    {
        boost::copy( range, std::front_inserter( result ) );
    }
    else
    {
        boost::copy( range, std::back_inserter( result ) );
    }

    if ( result.size() != 6 )
    {
        throw std::runtime_error( "the given address is malformed." );
    }

    return boost::copy( result, output_iterator );
}


/**
 * イーサネットヘッダを作ってイテレータに書き込む
 */
template < typename OutputIterator >
void generate_ethernet_header(
    std::string const   &source,
    std::string const   &destination,
    std::uint32_t const &type,
    OutputIterator      output_iterator,
    bool const          reverse_byte_order = false
)
{
    output_iterator = tokenize_and_copy( destination, output_iterator, reverse_byte_order );
    output_iterator = tokenize_and_copy( source, output_iterator, reverse_byte_order );

    const char type_array[] = { static_cast< char >( type & 0x00FF ), static_cast< char >( type >> 8 ) };
    *output_iterator++ = type_array[ ( reverse_byte_order ? 0 : 1 ) ];
    *output_iterator++ = type_array[ ( reverse_byte_order ? 1 : 0 ) ];
}


using EndpointType = boost::asio::generic::raw_protocol::endpoint;

/**
 * ネットワークインターフェース名から endpoint を作る
 */
EndpointType create_ethernet_endpoint( std::string const &ifname )
{
    struct sockaddr_ll sll;
    memset( &sll, 0, sizeof( sll ) );
    sll.sll_ifindex = if_nametoindex( ifname.c_str() );

    return std::move(
        EndpointType( reinterpret_cast< EndpointType::data_type * >( &sll ), sizeof( sll ) )
    );
}


int main()
{
    using IoServiceType = boost::asio::io_service;
    using ProtocolType  = boost::asio::generic::raw_protocol;
    using SocketType    = boost::asio::basic_raw_socket< ProtocolType >;

    IoServiceType io_service;
    SocketType    socket( io_service );
    ProtocolType  protocol( AF_PACKET, htons( ETH_P_ALL ) );

    boost::asio::streambuf buffer;
    std::ostream stream( &buffer );

    // イーサネットヘッダを書き込む
    generate_ethernet_header(
        "12:34:56:78:90:ab",
        "FF:FF:FF:FF:FF:FF",
        0x1001,
        std::ostream_iterator< char >( stream )
    );

    // データ部分を書き込む
    unsigned char const deadbeef[] = { 0xde, 0xad, 0xbe, 0xef };
    stream << "Hello, Ojisan!!!"
           << reinterpret_cast< char const * >( deadbeef );

    boost::system::error_code error;

    socket.open( protocol );
    auto const size = socket.send_to(
        buffer.data(),
        create_ethernet_endpoint( "enp3s0" ),
        0,
        error
    );

    std::cout << error << std::endl;
    return 0;
}

送信元 MAC アドレスは 12:34:56:78:90:ab, 宛先は FF:FF:FF:FF:FF:FF で、内容は "Hello, Ojisan!!!" という文字列と 0xdeadbeef のバイナリを入れて送信しました。イーサネットフレームのタイプは Wireshark でわかりやすいように適当に 0x1001 にしましたが、本当はここにのせるパケットのタイプを入れることは言うに及びませんね。

Screenshot from 2014-11-12 02:12:46.png

余談

python だと同じようなことするのに楽すぎてビビりまくりました ( さっきコードといろんな意味合いが違うので鬼不公平ですが…… )。C++ とは違った楽しさ。試しに書くのにちょうどいいですね:

import socket

source = b"\x11\x22\x33\x44\x55\xab"
destination = b"\xff\xff\xff\xff\xff\xff"
type = b"\x10\x01"
data = b"Hello, Ojisan!!!\xde\xad\xbe\xef"

socket = socket.socket(socket.AF_PACKET, socket.SOCK_RAW)
socket.bind(("enp3s0", 0))
payload = destination + source + type + data
socket.send(payload)
6
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
7