💎

【Ruby】ARPリクエストを送信する

2021/04/13に公開

概要

https://github.com/kuredev/simple_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権限が必要

sample_run.rb
require_relative "./lib/simple_arp"

client = SimpleARP::Client.new(src_if_name: "eth0", dst_ip_addr: "172.31.0.1")
client.send
console
% 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

https://github.com/kuredev/simple_arp/blob/9bb0809de2d8fd3c481be36a6fd41bb257ac7543/lib/simple_arp/sock_address_ll.rb#L21-L32

送信データの準備

上記のソケットを利用して最終的には socket.send(data, 0) します。
データは struct ether_headerstruct ether_arp をつなぎ合わせたものを準備します。このsendの引数も構造体をpackした文字列を準備しますので、上記「ソケットのバインド」で見たように対象の構造体に合わせてデータをpackした文字列をつなぎ合わせます。

例えば、 struct ether_header の方は以下のように構成しました。
例えば、dhost はARPリクエストはブロードキャストとなるため、 255 をpackします。
送信データはネットワークバイトオーダー(ビッグエンディアン)になることに注意します。

ether_header
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 */
}
Rubyコード
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

https://github.com/kuredev/simple_arp/blob/9bb0809de2d8fd3c481be36a6fd41bb257ac7543/lib/simple_arp/ether_header.rb#L15-L21

データ送信時の第3引数

ところでRubyのBasicSocket#sendは第3引数を取ることが出来ます。これはC言語(システムコール)でいうところの sendto を送信することと同様になるようです。

https://qiita.com/kure/items/6334a3c066f74a795a81

send はよく connect されているときに使用可能、といった説明を見ます。今回はconnectしていたわけではありませんが、RAWソケットでイーサヘッダから指定しており、宛先アドレスも含めてデータ部で指定していたからか、send で問題無く動作しました。
ちなみに第3引数を入れて、sendtoで送ったときも同様に送信できることを確認しました。
動作的な違いは無いようでしたが、どちらがベター等あるのかはよく分かりません。 🤔

socket.send(data, 0, SimpleARP::SockAddressLL.new(@src_if_name).to_pack_to)

https://github.com/kuredev/simple_arp/blob/9bb0809de2d8fd3c481be36a6fd41bb257ac7543/lib/simple_arp/client.rb#L31

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のときはそれを外しています。
https://github.com/kuredev/simple_arp/blob/0ded959b51739ce834d5f8449cb6369e2b6142cb/lib/simple_arp/client.rb#L38

参考

https://amzn.to/3smaQBP

https://amzn.to/3daPu5W

http://www.furuta.com/yasunori/linux/packet_socket.html

Discussion