PHPのsocket拡張を使うと、簡単にネットワークプログラミングができ、RAWソケットも利用可能なためTCPやIPパケットも読めます。
PHPのsocket拡張は、中ではCのsocketを呼び出しているだけなのですが、なぜかプロトコルファミリーでAF_PACKET指定できず、イーサネットフレームの読み書きはできませんでした。
PHP8.5からはこちらのPull Requestが取り込まれておりAF_PACKETが使えるようになりました!
世界はやはり広いですね!まさか自分以外にPHPでAF_PACKETを使いたい人がいて、本家のコードにPull Requestを送る人がいるとは。
利用方法
基本的にはAF_PACKETとSOCK_RAWを指定すればイーサネットフレームが読めます。
$socket = socket_create(AF_PACKET, SOCK_RAW, ETH_P_IP);
$data = @socket_read($socket, 8000);
これだけです!お手軽ですね。
イーサネットフレームの書き込み
自身でイーサネットフレームを作ってこのsocketに書き込みをしても実際にはデータは送信されません。どのネットワークインターフェースから送信するか指定が必要になるからです。
そこでLinuxの SO_BINDTODEVICE というオプションを使い、利用するNICを紐づけることができるのですが、イーサネットフレームを書き込むと No such device or address というエラーが出てしまいできませんでした。
(socket_set_optionの実行自体はtrueが返って成功するのですが)
// こちらの方法では使えなかった
socket_set_option($socket, SOL_SOCKET, SO_BINDTODEVICE, "eth0");
結論としては、NICをbindしてあげればイーサネットフレームの書き込みもできました。
socket_bind($this->socket0, 'eth0');
このPHPのsocket_bind関数の第2引数は、IPアドレスを指定するのですがここにNICの名前を指定することでsocketにネットワークインターフェースが紐付けできます。
よかったですね!🎉
今回の調査方法
今回の調査では、まずはC言語レベルで実装する方法を理解し、それがPHPのsocket拡張の実装でどうなっていて使えるかという追い方をしました。(PHPの実装はC言語のためC言語からやり方を追うとはやい)
リンク層(イーサネットフレームなど)をsocketで扱う時に、特定のNICからデータの送受信をしたい場合は、 sockaddr_ll 構造体を使います。
sockaddr_ll.sll_ifindex = if_nametoindex('eth0')
のように、sll_ifindexにNICの情報を付与し、sockaddr_ll構造体をsocketにbindします。(Cのbind関数を使うだけ)
PHPのsocket拡張はC言語で実装されているので、bind()やsockaddr_ll などでコードを検索しました。
https://github.com/php/php-src/blob/php-8.5.0beta2/ext/sockets/sockets.c#L1402
このあたりが怪しいと目星をつけたところ、PHPのsocket_bind関数でした。ここの第2引数はIPアドレスを指定するとドキュメントにはあるのですが、実際のコードを見るとAF_PACKETの場合は
sa->sll_ifindex = if_nametoindex(ZSTR_VAL(addr));
こちらのようなコードになっていて、addrに'eth0'のようなNIC名を渡せばsll_ifindexに紐付けできるとわかりました。
if_nametoindex()はこちらを読むと動きがわかります。