【Ruby】ARPリクエストを送信する
概要
RubyでARPリクエストを送信するプログラムを書いてみました。
ARPリクエストはソケットの種類的には、以下の2通りの書き方があるようです。
今回それぞれのソケットで書いてみました。
- AF_PACKET + SOCK_RAW(イーサヘッダから書く)
- AF_PACKET + SOCK_DGRAM(ARPヘッダから書く)
環境
- Ruby 2.7
- Linux 4.14.219-164.354.amzn2.aarch64
SOCK_RAW
使い方
※RAWソケットのため実行にはRoot権限が必要
require_relative "./lib/simple_arp"
client = SimpleARP::Client.new(src_if_name: "eth0", dst_ip_addr: "172.31.0.1")
client.send
% sudo ruby sample_run.rb
実装メモ
ソケットの準備
準備するソケットは以下の通りです。ETH_P_ARP は独自に定義した定数です。
AF_PACKET + SOCK_RAWの組み合わせだと、イーサヘッダからデータを組み立てる(=ヘッダ中に指定するプロトコルも指定する)ので、実際には第3引数は何を入れても良さそう(?)に挙動的には思えたのですが、一応ARP用の値を入れています。
def socket
@socket ||= Socket.open(
Socket::AF_PACKET,
Socket::SOCK_RAW,
ETH_P_ARP # 1544
)
end
ソケットのバインド
ソケットをインターフェースにバインドします。以下によると、必ずしもバインドする必要は無いのかもしれませんが、複数インターフェースがある場合を想定して、特定のインターフェースにバインドしてみようと思います。
デフォルトでは、指定したプロトコル型のパケットはすべて packet ソケットに送られる。特定のインターフェースからのパケットだけを 取得したい場合には、 struct sockaddr_ll にアドレスを指定して bind(2) を呼び、 packet ソケットをそのインターフェースに結び付ける (バインドする)。
http://ja.manpages.org/af_packet/7
struct sockaddr_ll
は以下の通りです。各メンバを見ればわかる通り、物理インターフェースを表現できる構造になっています。
struct sockaddr_ll {
unsigned short sll_family;
unsigned short sll_protocol;
int sll_ifindex;
unsigned short sll_hatype;
unsigned char sll_pkttype;
unsigned char sll_halen;
unsigned char sll_addr[8];
};
ソケットにバインドするデータは、以下の形で準備します。
ソケットアドレス構造体を pack した文字列socket/ソケットアドレス構造体を pack した文字列もしくはAddrinfoオブジェクトを指定します。
https://docs.ruby-lang.org/ja/latest/method/Socket/i/bind.html
実際のデータを構成する部分は以下のようにしてみました。
各項目を型に合わせてArray#packして準備します。
def to_pack_from
sll_family = [Socket::AF_PACKET].pack("S")
sll_protocol = [0x0806].pack("S>") # htons(ETH_P_ARP)
sll_ifindex = [if_name_to_index(@if_name)].pack("i")
sll_hatype = [ARPHRD_ETHER].pack("S")
sll_pkttype = [PACKET_BROADCAST].pack("C")
sll_halen = [ETH_ALEN].pack("C")
sll_addr = if_name_to_mac_adress(@if_name) + [0].pack("C") * 2
sll_family + sll_protocol + sll_ifindex + sll_hatype + \
sll_pkttype + sll_halen + sll_addr
end
送信データの準備
上記のソケットを利用して最終的には socket.send(data, 0)
します。
データは struct ether_header
と struct ether_arp
をつなぎ合わせたものを準備します。このsendの引数も構造体をpackした文字列を準備しますので、上記「ソケットのバインド」で見たように対象の構造体に合わせてデータをpackした文字列をつなぎ合わせます。
例えば、 struct ether_header
の方は以下のように構成しました。
例えば、dhost はARPリクエストはブロードキャストとなるため、 255 をpackします。
送信データはネットワークバイトオーダー(ビッグエンディアン)になることに注意します。
struct ether_header
{
u_int8_t ether_dhost[ETH_ALEN]; /* destination eth addr */
u_int8_t ether_shost[ETH_ALEN]; /* source ether addr */
u_int16_t ether_type; /* packet type ID field */
}
def to_pack
ether_dhost = [255].pack("C") * 6
ether_shost = if_name_to_mac_adress(@if_name)
ether_type = [ETH_TYPE_NUMBER_ARP].pack("S>")
ether_dhost + ether_shost + ether_type
end
データ送信時の第3引数
ところでRubyのBasicSocket#sendは第3引数を取ることが出来ます。これはC言語(システムコール)でいうところの sendto を送信することと同様になるようです。
send はよく connect されているときに使用可能、といった説明を見ます。今回はconnectしていたわけではありませんが、RAWソケットでイーサヘッダから指定しており、宛先アドレスも含めてデータ部で指定していたからか、send で問題無く動作しました。
ちなみに第3引数を入れて、sendtoで送ったときも同様に送信できることを確認しました。
動作的な違いは無いようでしたが、どちらがベター等あるのかはよく分かりません。 🤔
socket.send(data, 0, SimpleARP::SockAddressLL.new(@src_if_name).to_pack_to)
SOCK_DGRAM
以下のようにソケットを準備します。
def socket_dgram
@socket ||= Socket.open(
Socket::AF_PACKET,
Socket::SOCK_DGRAM,
ETH_P_ARP # 1544
)
end
RAWソケットとの違いは、データ送信時のデータにイーサヘッダが不要なことです。
RAWソケット時は ether_header
を送信していましたが、SOCK_DGRAMのときはそれを外しています。
参考
Discussion