【Linux】同じネットワーク内の機器のipアドレスを全てリストアップするシェルスクリプト

2023-12-09プログラミング

同じネットワーク内(サブネットマスク255.255.255.0限定)に接続されている機器のIPアドレスとホスト名を全て列挙するシェルスクリプトを作成しました。

同じことができるコマンドとして、arpコマンドは有名ですが、arpだと一度自分自身と何かしらの通信をしたことが無いとarpテーブルに登録されないので全て出てくるとは限りません

通信したことがある、無いにかかわらず全ての機器のIPアドレスを列挙したいときに便利です。

また、サブネットマスクは分かるがIPアドレスが不明の機器のIPアドレスを調べるのにも役に立ちます。

※できるだけ簡素にするため、エラー処理を記述していません。あくまで単発評価用ですのでその点ご容赦ください。

実行例

以下のようにIPアドレスのサブネット部(ここでは192.168.10.xxxのxxx以外の部分)を第1引数にするとそのサブネット内に存在するIPアドレスとホスト名を列挙してくれます。

./ipsearch.sh 192.168.10.       # サブネットマスクは24bit(255.255.255.0)限定

bash前提なので、dashやsource、「.」などで動かしたら動きません。Ubuntuだとshはdashにシンボリックリンクが張られているので、shでも意図通り動かないです。必ずbashをつけるか、./ipsearch.sh と直接パス指定のみで実行してください。

### IP address list : 
192.168.10.1	: aterm.me.
192.168.10.104	: 3(NXDOMAIN)
192.168.10.105	: 3(NXDOMAIN)
192.168.10.106	: 3(NXDOMAIN)
192.168.10.110	: 3(NXDOMAIN)
192.168.10.112	: <My PC Name>

このようにルーターや自身のIP含め、同じネットワーク内の機器のIPアドレスとホスト名を全て列挙してくれます。

しかしホスト名に関してはDNSサーバーに登録されていないと上のように(NXDOMAIN)と出てきてしまいます。

ソースコード

以下がソースコードです。

#!/bin/bash

# IPアドレスを検索する関数
search_ip() {
  local ipaddr=$1
  if ping -c 1 -w 1 -i 1 $ipaddr >> /dev/null  2>&1; then
    echo -e "$ipaddr\t: `host $ipaddr | awk '{print $NF}'`" | tr '\n' ' ' | sed 's/ $/\n/'
  fi
}

# IPアドレスリストを作成する関数
create_iplist() {
  local subnet=$1
  local tmpfile=$2
  for i in {1..254}; do
    ipaddr="$subnet$i"
    search_ip $ipaddr >> $tmpfile &
  done
  wait
}

# メイン処理
trap 'echo "Interrupted"; break' SIGINT

tmpfile="/tmp/ipsearch.tmp"
rm $tmpfile > /dev/null 2>&1

# IPアドレスリストを作成する
create_iplist $1 $tmpfile

# IPアドレスリストを表示する
echo "### IP address list : "
#sort $tmpfile | uniq
sort -t . -k 1,1n -k 2,2n -k 3,3n -k 4,4n $tmpfile
rm $tmpfile > /dev/null 2>&1

プログラムの流れ

仕組みは単純です。IPアドレスのネットワーク部(例えば192.168.10.○○○)を受け取って、ホスト部に1, 2, 3, … ,254を入れてpingコマンドを打っているだけです。応答が帰ってきたらそのIPアドレスを登録し、タイムアウト時間(1秒)内に帰ってこなかったら登録しない、という形です。

また、IPアドレスの登録は一時ファイル /tmp/ipsearch.tmp に書き込む形とし、シェルが終わると削除する仕組みにしています。またこの/tmp ディレクトリはLinuxではtmpfsと呼ばれるもので、揮発性のディレクトリです。Linuxを再起動すると、/tmpの中身は自動削除されますので、シェルが異常終了して一時ファイルが残ったとしても永久に残り続けることはありません。

ただしpingコマンドの最低タイムアウト時間は1秒なので順番に実行していると、255秒かかってしまいます。そこで今回は、『pingして応答があったIPアドレス /tmp/ipsearch.tmp に追記する』という内容を関数化し、その関数をサブプロセス(子プロセス)として1 ~ 255まで並列実行する形を取りました。

環境にもよりますが、筆者の環境だと1.3秒弱で実行できました。

time ./ipsearch.sh 192.168.10.       # time <COMMAND>でコマンドの実行時間測定
### IP address list : 
192.168.10.1	: aterm.me.
192.168.10.104	: 3(NXDOMAIN)
192.168.10.105	: 3(NXDOMAIN)
192.168.10.106	: 3(NXDOMAIN)
192.168.10.110	: 3(NXDOMAIN)
192.168.10.112	: <My PC Name>

real	0m1.248s
user	0m0.757s
sys	  0m0.369s

ソースコード解説

メインの処理から解説していきます。

trapコマンドでシェルをCtrl + Cで強制終了できるようにする

trap 'echo "Interrupted"; break' SIGINT

今回のシェルスクリプトはサブプロセス(子プロセス)を255個作ります。Ctrl + Cを実行すると、親プロセスは終了できますが、生み出された子プロセスは動き続けます。

そこで親プロセスが強制終了のシグナルを受け取ると、子プロセスも含めて強制終了できるようにtrapコマンドを記入します。trapコマンドのフォーマットは以下です。

trap <command> <signal>

ソースコードの例だと、Ctrl + Cが実行された場合、SIGINTという中断のシグナルを受け取ったら、echo “Interrupted"コマンドを実行し、メッセージを表示した上でbreakコマンドでfor文を強制終了します。

IPアドレスリストを格納する一時ファイルを作成

tmpfile="/tmp/ipsearch.tmp"
rm $tmpfile > /dev/null 2>&1

サブプロセスの結果を受け取るには、親と子の間にプロセス間通信を生成せねばなりません。プロセス間通信は、例えばパイプライン、ソケット、キュー、共有メモリなどがありますが一番簡単なのは一時ファイルを作って全員でそこに書き込みに行く形です。ここでは/tmp/ipsearch.tmpという一時ファイルに書き込むことにします。初期処理として、前回使った/tmp/ipsearch.tmprmコマンドで削除しておきます。rmコマンドの標準出力や標準エラー出力は邪魔なので両方とも/dev/nullに捨てます。2>&1という表記はは標準エラー出力を標準出力にリダイレクトすることを示します。

IPアドレスリストを作成

さて、一番重要なIPアドレスリストを作成する処理ですが、以下のように関数化しています。

create_iplist()        # IPアドレスのhost部を1~254まで振ってsearch_ip()を呼び、結果を/tmp/ipsearch.tmpに書き込む
    |-> search_ip()    # 指定のIPアドレスにpingを打ち、返答があればIPアドレスとhost名の文字列をechoする

search_ip()関数:IPアドレス検索

# IPアドレスを検索する関数
search_ip() {
  local ipaddr=$1
  if ping -c 1 -w 1 -i 1 $ipaddr >> /dev/null  2>&1; then
    echo -e "$ipaddr\t: `host $ipaddr | awk '{print $NF}'`" | tr '\n' ' ' | sed 's/ $/\n/'
  fi
}

IPアドレス(例:192.168.10.12)を第1引数($1)として受け取って、pingコマンドを打ちます。pingコマンドは応答が返ってきたら0を返し、タイムアウトなどの場合は1を返します。シェルスクリプトにおいて、if文の条件式に指定されたコマンドが終了ステータス0で終了した場合に真となり、それ以外は偽となります。他の言語の場合は、条件式に0だとfalseを意味しますが、シェルスクリプトにおいては終了ステータス0がtrueを意味します。

また、pingコマンドの引数の意味は以下です。また、pingコマンドの出力が邪魔なので例によって/dev/null >> 2>&1 で捨てています。

 ping -c 1 -w 1 -i 1 <IP address>
# -c 1 : 1回だけICMPプロトコルを実行
# -w 1 : タイムアウト時間を1秒とする
# <IP address> : IP address

次の行が少し難しいです。

echo -e "$ipaddr\t: `host $ipaddr | awk '{print $NF}'`" | tr '\n' ' ' | sed 's/ $/\n/'

まず、host $ipaddr でDNSサーバーに問い合わせてIPアドレスのホスト名を取得します。更に出力結果はホスト名のほかにもいろいろ情報がスペース区切りで出てきます。しかしホスト名は最後のフィールドなので、awk '{print $NF}’ コマンドをパイプで実行し、ホスト名の最後のフィールド(スペースで区切られた最後の文字列)を抽出します。$NF は AWK の予約変数で、現在の行の最後のフィールドを表します。

そのうえでIPアドレス$ipaddrと文字列を結合してechoしています。echoコマンドの-eオプションは、引用符で囲まれた文字列内にエスケープ文字を含んでいる場合に、それらのエスケープ文字を解釈して出力するためのオプションです。

しかしこれだけではまだ不十分です。ホスト名が複数登録されている場合もあるため、その場合、コマンドの出力は改行されてしまいます。その場合、見栄えが悪くなるのでまずtr '\n’ ' 'コマンドで改行コード\nをスペース「 」に変換します。

しかしこれだと末尾の改行コードもスペースに変換されてしまうので、sedコマンドを使って末尾のスペースを改行コード\nに変換します。

s/ $/\n/という部分は、次のように解釈できます。

  • s/ : substitute(置換)の意味で、sedの置換コマンドの開始を示す。
  •  : 置換対象の文字列。スペースを表しています。
  • $ : 行末のマーカー。つまり、行末にあるスペースを指定しています。
  • / : 次の置換対象文字列を指定するためのセパレータ。
  • \n : 置換後の文字列。改行を表しています。

つまり、このコマンドは、末尾にあるスペースを改行に置換することで、文字列内の改行をスペースに変換した後でも、末尾の改行コードを残すことができるようになります。

create_iplist() : IPアドレスリストを作成

# IPアドレスリストを作成する関数
create_iplist() {
  local subnet=$1
  local tmpfile=$2
  for i in {1..254}; do
    ipaddr="$subnet$i"
    search_ip $ipaddr >> $tmpfile &
  done
  wait
}

こちらは先ほど作ったsearch_ip()関数を254回 for文で呼んで、結果を一時ファイルに追記していきます。

ここで重要なのは、search_ip()関数の実行時、末尾に&をつけることで関数をサブプロセスとして実行します。

さらにwaitを記述することですべてのサブプロセスが終了するまで待ちます。これがないと、親プロセスが次に進んでしまいます

IPアドレスリストを表示する

さて、親プロセスに戻ってきます。

# IPアドレスリストを表示する
echo "### IP address list : "
#sort $tmpfile | uniq
sort -t . -k 1,1n -k 2,2n -k 3,3n -k 4,4n $tmpfile
rm $tmpfile > /dev/null 2>&1

さて、IPアドレスリストができたので、表示させます。/tmp/ipsearch.tmpの内容を表示させればいいのですが、中身は順番ではありません。サブプロセスが各々のタイミングで追記していっているためです。よって表示させるときは、sortコマンドでIPアドレス順に並べています。

sort -t . -k 1,1n -k 2,2n -k 3,3n -k 4,4nで、指定されたファイル $tmpfile 内のIPv4アドレスを並び替えるためのものです。具体的には、.(ドット)で区切られた4つの数値を持つIPv4アドレスを、各数値を単位として数値的に昇順にソートすることで並び替えます。ソートコマンドのデフォルトは辞書式ソートなのでオプション無しだと192.168.10.102の後に、192.168.10.1が来てしまいます。

オプションの解説を行います。

  • -t .:フィールドの区切り文字を.(ドット)に指定します。
  • -k 1,1n:最初のフィールド(IPv4アドレスの1つ目の数値)を、数値として昇順にソートします。,で区切って、1から1の範囲を指定しています。
  • -k 2,2n:2番目のフィールドを、同様に数値として昇順にソートします。
  • -k 3,3n:3番目のフィールドを、同様に数値として昇順にソートします。
  • -k 4,4n:4番目のフィールドを、同様に数値として昇順にソートします。

つまり、このコマンドは、IPv4アドレスの1つ目の数値から順に比較し、同じ場合には2つ目の数値を比較し、さらに同じ場合には3つ目、4つ目の数値を比較して昇順に並び替えます。

さて、ソートが終わったら、最後、一時ファイルを削除して完了です。

以上!

【追記】nmapコマンドを使えば同じことができる!

この記事を書いた翌日、同じ事ができるコマンドが存在することが判明。それがnmapコマンドです。

仕組みとしては本記事と同じで、内部でpingを送信しているだけです。

sudo apt-get install nmap

以下のようにコマンドを送ると、MACアドレスも一緒に出してくれます。

nmap -sn 192.168.1.0/24
Starting Nmap 7.80 ( https://nmap.org ) at 2023-03-12 08:57 JST
Nmap scan report for aterm.me (192.168.10.1)
Host is up (0.0022s latency).
MAC Address: 08:15:5A:8D:3B:9C (Unknown)
Nmap scan report for 192.168.10.104
Host is up (0.0096s latency).
MAC Address: 60:5A:C3:1B:F5:0D (Intel Corporate)
Nmap scan report for 192.168.10.105
Host is up (0.011s latency).
MAC Address: A9:0F:2B:6C:9E:D7 (Unknown)
Nmap scan report for 192.168.10.106
Host is up (0.033s latency).
MAC Address: 6C:1B:9E:8D:C2:0F (Unknown)
Nmap scan report for 192.168.10.110
Host is up (0.064s latency).
MAC Address: F2:A4:0E:3D:8C:9B (Seiko Epson)
Nmap scan report for <My PC> (192.168.10.112)
Host is up.
Nmap done: 256 IP addresses (7 hosts up) scanned in 1.73 seconds

参考文献

2023-12-09プログラミング

Posted by tech-biz-creator