ARP requestを実際に送ってIPアドレスをMACアドレスに解決する
指定したIPアドレスに対してARP requestを送信し、応答(reply)を受け取ってMACアドレスを解決するarp_resolveというプログラムを実装しました。ARPパケットの受信・解析だけでなく、送信までを含めた一連の流れが実機で確認できるようになります。
Ethernetヘッダを手で組み立てて送信する
ARPパケット本体を組み立てるbuild_arp_request()は28バイトのARPペイロードを返しますが、実際にNICから送信するには、その手前に14バイトのEthernetヘッダ(宛先MAC・送信元MAC・EtherType)を付ける必要があります。今のところEthernetヘッダを組み立てる関数は用意していないため、送信するその場でバイト列を直接組み立てています。
// l2-arp/apps/arp_resolve.cpp
auto arp_payload = l2arp::build_arp_request(*sender_mac, *sender_ip, *target_ip);
std::array<std::byte, 14 + arp_payload.size()> frame{};
std::ranges::fill(std::span(frame).subspan<0, 6>(), std::byte{0xFF}); // 宛先: broadcast
std::ranges::copy(*sender_mac, frame.begin() + 6); // 送信元MAC
frame[12] = std::byte{0x08};
frame[13] = std::byte{0x06}; // EtherType = ARP(0x0806)
std::ranges::copy(arp_payload, frame.begin() + 14);
socket->send(frame);ARP requestは相手のMACアドレスを知らない状態で送るものなので、宛先MACは全ビット1(ff:ff:ff:ff:ff:ff)のブロードキャストにします。同一LAN上の全ホストがこのフレームを受け取り、target_ipを持つホストだけが応答(reply)を返す、という仕組みです。
ioctlでローカルのMAC/IPアドレスを取得する
ARP requestの送信元情報(sender_mac/sender_ip)には、このプログラムを実行しているマシン自身のMAC/IPアドレスが必要です。これを毎回コマンドライン引数で手入力するのは面倒なので、ioctlでインターフェース名から自動取得するようにしました。
// l2-arp/apps/arp_resolve.cpp(抜粋)
std::optional<l2arp::MacAddress> local_mac(std::string_view interface_name)
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd == -1)
{
return std::nullopt;
}
ifreq ifr{};
std::string name(interface_name);
std::strncpy(ifr.ifr_name, name.c_str(), IFNAMSIZ - 1);
bool ok = ioctl(fd, SIOCGIFHWADDR, &ifr) != -1;
close(fd);
if (!ok)
{
return std::nullopt;
}
l2arp::MacAddress mac{};
std::memcpy(mac.data(), ifr.ifr_hwaddr.sa_data, mac.size());
return mac;
}ioctlにifreq構造体(インターフェース名を詰めたもの)を渡して呼ぶと、カーネルがそのインターフェースの情報を埋めて返してくれます。SIOCGIFHWADDRでMACアドレス、SIOCGIFADDRでIPv4アドレスが取得できます。ここで開いているAF_INETのソケットは実際の通信には使わず、ioctlを呼ぶための一時的なハンドルとして使っているだけです。
受信したパケットが「本当に待っていたreply」かを確認する
送信後は受信ループに入り、ARPパケットを見つけるたびに中身を検証します。ここで重要なのが、単に「ARPパケットが来たら即採用」ではなく、次の条件を両方満たすかをチェックしている点です。
// l2-arp/apps/arp_resolve.cpp(抜粋)
auto arp = l2arp::parse_arp_packet(eth_frame->payload);
if (!arp || arp->operation != l2arp::ArpOperation::Reply || arp->sender_ip != *target_ip)
{
continue;
}AF_PACKETのraw socketは、多くの場合そのインターフェースから自分自身が送信したフレームのコピーも受信対象に含みます。つまりこのチェックを省略すると、送信した直後に自分自身のARP request(operation == Request、sender_ip = 自分自身のIP)を受信・パースしてしまい、「解決できた」と誤判定して自分自身のIP/MACを表示してしまう可能性があります。同一LAN上を流れる他のARPトラフィック(無関係なホスト同士のRequest/Reply)を拾ってしまう可能性も同様です。operationがReplyであること、かつsender_ipが今探しているtarget_ipと一致することの両方を確認して初めて、それが自分の送ったrequestに対する応答だと判断できます。
実機での動作確認
実機でsudo権限付きで実行し、同一LAN上の実在するホストのIPアドレスを指定すると、ARP requestを送信した直後にreplyを受信し、MACアドレスが正しく解決されることを確認できました。
// l2-arp/apps/arp_resolve.cpp(main関数の全体像)
auto sender_mac = local_mac(argv[1]);
auto sender_ip = local_ipv4(argv[1]);
auto target_ip = parse_ipv4(argv[2]);
// ... requestを送信 ...
l2arp::ArpCache cache;
while (true)
{
auto received = socket->receive(buffer);
auto eth_frame = l2arp::parse_ethernet_frame(*received);
if (!eth_frame || eth_frame->ether_type != l2arp::EtherType::Arp) continue;
auto arp = l2arp::parse_arp_packet(eth_frame->payload);
if (!arp || arp->operation != l2arp::ArpOperation::Reply || arp->sender_ip != *target_ip) continue;
cache.insert(arp->sender_ip, arp->sender_mac);
break;
}参考リンク
- Linux packet(7) man page ——
AF_PACKETソケットが送信済みフレームのコピーも受信しうる挙動を含む仕様です - RFC 826: An Ethernet Address Resolution Protocol —— ARP request/replyのプロトコル仕様です