はじめに
リンクレイヤーのパケットを扱いたいときがある、かと言われたらないかもしれませんが……。手探りの覚書として残しておきます。確認環境は clang on Linux です。
概説
C
ならばおもむろにソケットを開いて PF_PACKET
と SOCK_RAW
をぶち込めば、低層のパケットを扱うことができますので、C++
でも同様にして開けます。C++
チックになにかできないか探しました。
Boost.Asio
ならなんとかしてくれるに違いない。generic::raw_protocol
や basic_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_address
は struct 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
にしましたが、本当はここにのせるパケットのタイプを入れることは言うに及びませんね。
余談
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)