Code Day's Night

ichikawayのブログ

PHPで第2層イーサネットの読み書きはできるのか?(できる)

PHPのsocket拡張を使うと、簡単にネットワークプログラミングができ、RAWソケットも利用可能なためTCPやIPパケットも読めます。

 

PHPのsocket拡張は、中ではCのsocketを呼び出しているだけなのですが、なぜかプロトコルファミリーでAF_PACKET指定できず、イーサネットフレームの読み書きはできませんでした。

 

PHP8.5からはこちらのPull Requestが取り込まれておりAF_PACKETが使えるようになりました!

github.com

世界はやはり広いですね!まさか自分以外に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()はこちらを読むと動きがわかります。

manpages.ubuntu.com