追蹤連線區域網路裝置的Raspberry Pi掃描器

九月 8, 2017
Facebook
Twitter
圖片

Hep Svadja攝

我們要用Raspberry Pi來打造一臺網路掃描器,追蹤和區域網路連線。要做其實很簡單,而且因為我們運用的是高隱密性的ARP掃描,所以只要不讓網路負荷太重,多數人是察覺不到的。
這種掃描器能安靜待在角落,監控家用或辦公室無線網路,不知不覺中查出哪些主機連線進來。但我們不是這種人。我們是會搬出漆著耀眼紅色、形體巨大的七段顯示器的人,所以我們的網路掃描器會很引人注目。
跟著我們的步驟自己做一臺:

購買硬體

這個專題用到的硬體多數都相當容易取得。除了巨大的七段顯示器和搭配的控制器板之外,可能大部份(甚至全部)的材料在你的工作室裡已經有了。

只要極少的焊接就能組裝完成,唯一需要焊接的地方是把控制器板連接到七段顯示器。這些電路板採用齒形安裝孔,對習慣通孔元件的人而言可能較難上手。但是不用擔心,難度絕對不及SMD焊接,而且有很多好用的指南可以幫助你。

加裝第二個無線轉接器(非必要)

我們要在Raspberry Pi加裝第二個USB Wi-Fi轉接器,因為Raspberry Pi 3上的BCM43438 Wi-Fi晶片組目前不支援監控模式。

第二個轉接器對我們的網路掃描器而言不是必要的,因為我們主要用的是機上的Wi-Fi轉接器,但加裝的話,之後可以有更大的彈性對網路環境進行更深入的監控。USB無線轉接器便宜而且容易買到,所以除非你預算很低,否則沒理由不裝。

一般市面上的USB無線轉接器內部其實只有兩三種晶片組。麻煩的是我們要仔細挑選支援監控模式的無線轉接器,因為外觀相同的轉接器在內部可能採用完全不同的晶片組。但是轉接器很便宜,所以如果買錯,再買一次就好。多一臺USB無線轉接器也是有備無患。
其中有些晶片組比較難用於監控模式運作。舉例來說,Realtek RTL8188CUS晶片雖然支援監控模式,而且開箱就能以Raspbian操作,但預設的Raspbian驅動程式不支援監控模式。要讓它運作是相當麻煩的事。
我們可以在多數Linux電腦輸入下列指令來檢查USB無線轉接器的晶片組:

 $ lsusb
 Bus 001 Device 004: ID 148f:5370 Ralink Technology, Corp. RT5370 Wireless Adapter
 Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
 Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp.
 Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
這個例子裡,我們用的USB無線轉接器裝有Ralink Technology RT5370晶片組,開箱即支援監控模式,而且具有同時支援監控和混和模式的罕見能力,所以可以看到單播、多播和廣播的訊框。如果買到採用這種晶片組的USB轉接器,就沒問題了。

取得Raspberry Pi的作業系統

首先要做的是完成Raspberry Pi的設定。這時要下載最新版的Raspbian Lite。把SD卡插入MacBook,開啟終端機視窗,並輸入df -h,接著記住SD卡的裝置名稱,例如這個例子裡是/dev/disk1。我們將會使用原始裝置/dev/disk1。
$ df -h
 Filesystem Size Used Avail Capacity iused ifree %iused Mounted on
 /dev/disk0s2 699Gi 367Gi 332Gi 53% 96214802 86992771 53% /
 devfs 206Ki 206Ki 0Bi 100% 714 0 100% /dev
 map -hosts 0Bi 0Bi 0Bi 100% 0 0 100% /net
 map auto_home 0Bi 0Bi 0Bi 100% 0 0 100% /home
 /dev/disk1s1 59Gi 33Gi 26Gi 57% 8739054 6768902 56% /Volumes/SD Card
把卡片卸載

 $ sudo diskutil unmount /dev/disk1s1

不是把它拖曳到垃圾桶來退出。接著在終端機視窗用下載的磁碟映像變更目錄,並輸入:

 $ unzip 2017-01-11-raspbian-jessie-lite.zip
 $ sudo dd bs=1m if=2017-01-11-raspbian-jessie-lite.img of=/dev/rdisk1

如果以上的指令回報錯誤「dd: bs: illegal numeric value」,就把bs=1m變更為bs=1M。dd完成後,卡片會自動重新掛載映像的開機磁碟區。因為近期的Raspbian作業系統版本讓SSH在第一次開機停用,而我們要在沒有螢幕或鍵盤的條件下執行,所以要重新啟用它。
前往開機磁碟區並輸入以下:

 $ cd /Volumes/boot
 $ touch ssh
 $ cd ~

ssh檔案的內容不重要。當Pi初次開機,它會尋找這個檔案;找到後,它會啟用SSH並刪除檔案。
用以下指令退出卡片

$ sudo diskutil eject /dev/rdisk1

或者,如果你不想用dd,我建議用Etcher應用程式來把SD卡映像燒到Mac上。

Raspberry Pi開機

把SD卡插入開發板、插入Wi-Fi轉接器,並且暫時把開發板接上乙太網路。然後接上電源讓它開機。紅色LED會先亮,接著綠色ACT燈會閃爍,橘色FDX和LNK燈會亮起,代表開機順利。
開機後,Raspberry Pi會用預設名稱raspberrypi.local透過mDNS來發出訊號。找到它最簡單的方法就是看它對ping有沒有回應
$ ping raspberrypi.local
 PING raspberrypi.local (192.168.1.159): 56 data bytes
 64 bytes from 192.168.1.159: icmp_seq=0 ttl=64 time=4.079 ms
 64 bytes from 192.168.1.159: icmp_seq=1 ttl=64 time=4.223 ms
 64 bytes from 192.168.1.159: icmp_seq=2 ttl=64 time=6.717 ms
 ^C
 — raspberrypi.local ping statistics —
 3 packets transmitted, 3 packets received, 0.0% packet loss
 round-trip min/avg/max/stddev = 4.079/5.006/6.717/1.211 ms

如果Raspberry Pi對ping沒有回應,第二簡單的方法是登入網路路由器,搜尋它被DHCP分配的IP位址。

設定Raspberry Pi

找到Pi之後,就用ssh登入。預設使用者名稱和密碼分別是pi和raspberry,接著設定讓它準備就緒。在看到啟動Raspbian設定公用程式的提示時輸入:

% sudo raspi-config
 
這個指令會開啟設定管理員。往下數第一個選項是Expand Filesystem(擴充檔案系統),它會自動神奇地擴充磁碟區的空間。接著向下捲動到Advanced Options(進階選項),並把主機名稱改成比較好認的,例如我取的是netscan。

Advanced Options裡面還有啟用SSH、SPI、I2C、和Serial的開關,現在就把它們都啟用。藉著設定公用程式開啟的機會,記得把使用者密碼改得安全一點。最後按下Finish(完成),並讓Raspberry Pi重新啟動。
登入後,就可以把Raspberry Pi更新到最新版本。輸入以下來更新到最新的安裝套件除錯版本:

 $ sudo apt-get update
 $ sudo apt-get upgrade

如果使用舊版的Raspbian,可以再輸入

 $ sudo apt-get dist-upgrade

來升級到作業系統最新版本。

關於我們的無線網路

基本上,我們要把Raspberry Pi機上的無線轉接器(wlan0)切換到混合模式,讓我們可以擷取我們連線到的網路上的封包。雖然我們的外接USB轉接器(wlan1)以監控模式運作,但這麼做就能無視連線的網路來擷取封包,也不需要連結存取點(也無需驗證)。
首先檢查目前的網路設定。看到提示時輸入:
 $ ifconfig
 eth0 Link encap:Ethernet HWaddr b8:27:eb:97:cc:e4
 inet addr:192.168.1.159 Bcast:192.168.1.255 Mask:255.255.255.0
 inet6 addr: fe80::b059:6454:1d28:763a/64 Scope:Link
 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
 RX packets:88128 errors:0 dropped:32 overruns:0 frame:0
 TX packets:45378 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:122233058 (116.5 MiB) TX bytes:3828475 (3.6 MiB)
 
 lo Link encap:Local Loopback
 inet addr:127.0.0.1 Mask:255.0.0.0
 inet6 addr: ::1/128 Scope:Host
 UP LOOPBACK RUNNING MTU:65536 Metric:1
 RX packets:38581 errors:0 dropped:0 overruns:0 frame:0
 TX packets:38581 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1
 RX bytes:3395128 (3.2 MiB) TX bytes:3395128 (3.2 MiB)
 
 wlan0 Link encap:Ethernet HWaddr b8:27:eb:c2:99:b1
 inet6 addr: fe80::9380:71d4:4917:9b65/64 Scope:Link
 UP BROADCAST MULTICAST MTU:1500 Metric:1
 RX packets:7 errors:0 dropped:7 overruns:0 frame:0
 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:773 (773.0 B) TX bytes:0 (0.0 B)
 
 wlan1 Link encap:Ethernet HWaddr 00:0f:60:05:a8:5c
 UP BROADCAST MULTICAST MTU:1500 Metric:1
 RX packets:0 errors:0 dropped:0 overruns:0 frame:0
 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
這裡可以看到回送網路裝置(lo)、目前用來連線到Pi的乙太網路(eth0)和它的IP位址、Raspberry Pi的機上無線轉接器(wlan0)和USB Wi-Fi轉接器(wlan1)。你會發現wlan0和wlan1都還沒設定,但後面就會設定了。
我們更仔細來看無線轉接器:
 $ iwconfig
 wlan0 IEEE 802.11bgn ESSID:off/any
 Mode:Managed Access Point: Not-Associated Tx-Power=31 dBm
 Retry short limit:7 RTS thr:off Fragment thr:off
 Power Management:on
 
 lo no wireless extensions.
 
 eth0 no wireless extensions.
 
 wlan1 IEEE 802.11bgn ESSID:off/any
 Mode:Managed Access Point: Not-Associated Tx-Power=20 dBm
 Retry short limit:7 RTS thr:off Fragment thr:off
 Power Management:off
 
 $ iw dev
 phy#1
 Interface wlan1
 ifindex 4
 wdev 0x100000001
 addr 00:0f:60:05:a8:5c
 type managed
 phy#0
 Interface wlan0
 ifindex 3
 wdev 0x1
 addr b8:27:eb:c2:99:b1
 type managed
我們會設定Raspberry Pi自己的無線轉接器(wlan0)來把Pi放上我們的網路,同時讓USB轉接器(wlan1)專門進行監控。為了以防萬一,再次檢查USB轉接器可以設定為監控模式:
 $ sudo iw phy phy1 interface add mon1 type monitor
 $ iw dev
 phy#1
 Interface mon1
 ifindex 5
 wdev 0x100000002
 addr 00:0f:60:05:a8:5c
 type monitor
 Interface wlan1
 ifindex 4
 wdev 0x100000001
 addr 00:0f:60:05:a8:5c
 type managed
 phy#0
 Interface wlan0
 ifindex 3
 wdev 0x1
 addr b8:27:eb:c2:99:b1
 type managed
如果看到mon1介面出現,代表一切運作順利,可以暫時用以下指令把介面拿掉:
$ sudo iw dev mon1 del

如果沒出現,就檢查晶片組是不是真的支援監控模式。輸入:
 $ iw phy phy1 info

在輸出內容中,會看到和以下很類似的東西:
 Supported interface modes:
 * IBSS
 * managed
 * AP
 * AP/VLAN
 * WDS
 * monitor
 * mesh point

其中監控(monitor)有被列為支援的介面模式。如果沒有列出,代表你的晶片組不支援監控模式,或核心驅動程式有問題。有些情況下,晶片組的主線核心驅動程式支援監控,但預設的Raspbian驅動程式沒有。

設定無線轉接器

現在用Raspberry Pi內建的無線轉接器連線到區域網路。首先要找到網路。輸入:
 $ sudo iwlist wlan0 scan

這個指令會掃描尋找網路,可能會找到好幾個,視你的所在位置而定。如果你不確定你的網路採用哪種加密,就找出長得像以下的這行:
 IE: IEEE 802.11i/WPA2 Version 1

有網路SSID和加密方法後,就可以讓Raspberry Pi連線。假設你用的是WPA2,就用喜歡的編輯器開啟/etc/wpa_supplicant/wpa-supplicant檔案:
 $ sudo nano /etc/wpa_supplicant/wpa_supplicant.conf
到檔案的底部輸入以下:
 network={
 ssid=”SSID”
 psk=”PASSWORD”
 }

SSID是你家用網路的ESSID,而PASSWORD是你的網路的WPA2密碼。另外,如果想要設定兩個(或以上)的無線網路,可以在每個網路加入id_str,如下所示:
 network={
 ssid=”OFFICE”
 psk=”OFFICE-PASSWORD”
 id_str=”office”
 }
 
 network={
 ssid=”HOME”
 psk=”HOME-PASSWORD”
 id_str=”home”
 }
開機後,Raspberry Pi會和其中一個網路連結。如果兩個網路都存在,可以新增優先順序,讓順序最高的網路先連線。
儲存設定檔後,wpa-supplicant可能會發現有設定變更,並在幾秒之內連線到你的(優先)無線網路。但一般而言不會這樣,所以要輸入以下:
 $ sudo ipdown wlan0
 $ sudo ifup wlan0
等待幾秒後輸入:
 $ ifconfig wlan0
 wlan0 Link encap:Ethernet HWaddr b8:27:eb:c2:99:b1
 inet addr:192.168.1.217 Bcast:192.168.1.255 Mask:255.255.255.0
 inet6 addr: fe80::9380:71d4:4917:9b65/64 Scope:Link
 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
 RX packets:441 errors:0 dropped:416 overruns:0 frame:0
 TX packets:20 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:126262 (123.3 KiB) TX bytes:3634 (3.5 KiB)
介面會取得IP位址。現在我們要設定USB無線轉接器。接下來我們的做法會有點不一樣。用你喜歡的編輯器開啟/etc/network/interfaces設定檔,並把wlan1條目變更為:
 allow-hotplug wlan1
 iface wlan1 inet manual
 pre-up iw phy phy1 interface add mon1 type monitor
 pre-up iw dev wlan1 del
 pre-up ifconfig mon1 up

如此會把管理介面拿掉,並在開機時把無線轉接器設為監控模式。完成變更後,輸入以下指令來重新啟動Pi:
 $ sudo reboot
Raspberry Pi重新啟動後,登入並檢查網路設定。應該會看到和以下很類似的內容:
 $ ifconfig
 eth0 Link encap:Ethernet HWaddr b8:27:eb:97:cc:e4
 inet addr:192.168.1.159 Bcast:192.168.1.255 Mask:255.255.255.0
 inet6 addr: fe80::b059:6454:1d28:763a/64 Scope:Link
 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
 RX packets:110 errors:0 dropped:0 overruns:0 frame:0
 TX packets:95 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:17019 (16.6 KiB) TX bytes:13954 (13.6 KiB)
 
 lo Link encap:Local Loopback
 inet addr:127.0.0.1 Mask:255.0.0.0
 inet6 addr: ::1/128 Scope:Host
 UP LOOPBACK RUNNING MTU:65536 Metric:1
 RX packets:0 errors:0 dropped:0 overruns:0 frame:0
 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1
 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
 
 mon1 Link encap:UNSPEC HWaddr 00-0F-60-05-A8-5C-30-30-00-00-00-00-00-00-00-00
 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
 RX packets:1414 errors:0 dropped:1414 overruns:0 frame:0
 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:335469 (327.6 KiB) TX bytes:0 (0.0 B)
 
 wlan0 Link encap:Ethernet HWaddr b8:27:eb:c2:99:b1
 inet addr:192.168.1.217 Bcast:192.168.1.255 Mask:255.255.255.0
 inet6 addr: fe80::9380:71d4:4917:9b65/64 Scope:Link
 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
 RX packets:134 errors:0 dropped:77 overruns:0 frame:0
 TX packets:31 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:30147 (29.4 KiB) TX bytes:5258 (5.1 KiB)”
檢查裝置時會看到:
 $ iw dev
 phy#1
 Interface mon1
 ifindex 5
 wdev 0x100000002
 addr 00:0f:60:05:a8:5c
 type monitor
 channel 1 (2412 MHz), width: 20 MHz (no HT), center1: 2412 MHz
 phy#0
 Interface wlan0
 ifindex 3
 wdev 0x1
 addr b8:27:eb:c2:99:b1
 ssid OFFICE
 type managed
依預設,wlan0連線到家用網路,而wlan1在監控模式,在Channel 1。
不用馬上進行,但可以變更頻道,做法是以MHGz指定中頻或頻道編號,也就是
 $ sudo iw dev mon1 set freq 2437

 $ sudo iwconfig mon1 channel 6
 $ iw dev
 phy#1
 Interface mon1
 ifindex 5
 wdev 0x100000002
 addr 00:0f:60:05:a8:5c
 type monitor
 channel 6 (2437 MHz), width: 20 MHz (no HT), center1: 2437 MHz
 phy#0
 Interface wlan0
 ifindex 3
 wdev 0x1
 addr b8:27:eb:c2:99:b1
 ssid OFFICE
 type managed

會把USB無線轉接器從Channel 1轉換到Channel 6。記得,
 $ sudo iwlist wlan0 scan
會產生附近無線網路的清單,包含這些網路使用的頻道的資訊。這代表我們都準備好了,可以把Raspberry Pi連線到wlan0上的已知網路(一個或多個),並以混和模式監控連線。我們還可以用mon1介面來監控其他無線網路的連線(或透過不同網路多個頻率間掃描)。
如果需要(暫時)把mon1復原到管理模式,可以用以下指令:
 $ sudo iw dev mon1 del
 $ sudo iw phy phy1 interface add wlan1 type managed

現在我們已經完成無線轉接器的設定了,可以放心拔除乙太網路纜線。除非要從有線網路存取Raspberry Pi,否則不會再用到了。

監控工具

Kismet是一種無線網路偵測器、監聽器和入侵偵測系統,和其他無線網路偵測器的不同點是它採被動運作。換句話說,它進行偵測的同時不會傳送任何可記錄的封包,能同時偵測無線存取點和無線用戶端的存在,並把兩者連結在一起。它是使用範圍最廣也最新的開源無線監控工具。
首先下載、建置和安裝Kismet。
$ sudo apt-get install git-core build-essential
 $ sudo apt-get install libncurses5-dev libpcap-dev libpcre3-dev libnl-dev libmicrohttpd10 libmicrohttpd-dev
 $ wget http://kismetwireless.net/code/kismet-2016-07-R1.tar.xz
 $ tar -xvf kismet-2016-07-R1.tar.xz
 $ cd kismet-2016-07-R1
 $ ./configure
 $ make
 $ sudo make suidinstall
 $ sudo usermod -a -G kismet pi
 $ sudo mkdir -p /usr/local/lib/kismet/
 $ mkdir -p /home/pi/.kismet/plugins/
 $ sudo mkdir -p /usr/lib/kismet/
 $ sudo reboot

Raspberry Pi重新啟動後,我們要對Kismet設定檔做一些小變更。用你喜歡的編輯器開啟/usr/local/etc/kismet.conf,並如下修改兩個條目:
 ncsource=mon1
 hidedata=true
接著要下載製造商清單,用來幫助Kismet辨識遇到各種網路裝置的製造商。
 $ sudo mkdir -p /usr/share/wireshark/
 $ cd /usr/share/wireshark/
 $ sudo wget -O manuf http://anonsvn.wireshark.org/wireshark/trunk/manuf
 $ sudo cp manuf /etc/manuf

完成Kismet設定後,可以直接從指令行啟動伺服器元件
 $ kismet_server

或做為守護程式來啟動
 $ kismet_server -c mon1 –daemonize

接著可以從指令行啟動kismet_client
 $ kismet_client
預設Kismet文字用戶端會隨即出現。
從用戶端連線到kismer_server來確認它運作正常後,我們可以開機時從/etc/rc.local啟動伺服器,讓它在開機時啟動。

這麼做之前,我們要啟用rc.local服務本身,做法是建立rc-local.service檔案
 $ sudo vi /etc/systemd/system/rc-local.service
檔案內容會像以下:
 [Unit]
 Description=/etc/rc.local Compatibility
 ConditionPathExists=/etc/rc.local
 
 [Service]
 Type=forking
 ExecStart=/etc/rc.local start
 TimeoutSec=0
 StandardOutput=tty
 RemainAfterExit=yes
 SysVStartPriority=99
 
 [Install]
 WantedBy=multi-user.target

儲存並關閉檔案,確認/etc/rc.local檔案為可執行。
 $ sudo chmod +x /etc/rc.local
並於系統開機啟用服務:
 $ sudo systemctl enable rc-local
 $ sudo systemctl status -l rc-local.service
 ● rc-local.service - /etc/rc.local Compatibility
 Loaded: loaded (/etc/systemd/system/rc-local.service; enabled)
 Drop-In: /etc/systemd/system/rc-local.service.d
 └─ttyoutput.conf
 Active: active (exited) since Thu 2017-02-02 12:08:33 UTC; 1min 56s ago
 
 Feb 02 12:08:33 netscan systemd[1]: Starting /etc/rc.local Compatibility…
 Feb 02 12:08:33 netscan systemd[1]: Started /etc/rc.local Compatibility.

如果一切順利,現在可以編輯預設/etc/rc.local讓kismet_server在開機時啟動。
 #!/bin/sh -e
 
 su pi -c ’/usr/local/bin/kismet_server -n -c mon1 –daemonize’
 exit 0
現在每次Pi開機時,Kismet伺服器會在背景啟動。注意到這裡用「-n」指令行選項來抑制記錄,如此就不會讓記錄資訊塞滿SD卡。
照常執行kismet_client就能用Pi以本機存取伺服器,也可以透過網路在netscan.local:2501存取。

掃描工具

現在我們已經準備好基本的監控功能(或許加上比較花俏的改造)之後,就要進入這個網路掃描器專題的重頭戲,並安裝一些工具來尋找和計算連線到家用或辦公室網路的裝置。首先要安裝nmap,接著是arp-scan。
 $ sudo apt-get install nmap
 $ sudo apt-get install arp-scan

你可能熟悉nmap,但arp-scan就不一定了。它是非常快速的ARP封包掃描器,會顯示區域網路上所有使用中的裝置。

這種工具有趣的地方是,即使有裝置不回應網路要求,而且有加密,都能被ARP掃描偵測到。缺點是,因為ARP屬於不選擇路徑的,這種掃描器只能用於區域網路區段。如果你的網路比較複雜,有多台路由器,就很難抓到網路上所有主機。
在我的區域網路執行arp-scan的結果如下:
 $ sudo arp-scan –retry=8 –ignoredups -I wlan0 –localnet
 Interface: wlan0, datalink type: EN10MB (Ethernet)
 Starting arp-scan 1.8.1 with 256 hosts (http://www.nta-monitor.com/tools/arp-scan/)
 192.168.1.93 70:73:cb:b2:91:ee (Unknown)
 192.168.1.130 70:73:cb:b2:91:ee (Unknown)
 192.168.1.91 e0:cb:ee:41:ce:29 (Unknown)
 192.168.1.208 70:73:cb:b2:91:ee (Unknown)
 192.168.1.253 9e:97:26:94:76:e4 (Unknown)
 192.168.1.254 9c:97:26:94:76:e4 (Unknown)
 192.168.1.131 f4:5c:89:8b:79:a7 (Unknown)
 192.168.1.120 e0:ac:cb:a3:1d:04 (Unknown)
 192.168.1.121 60:03:08:aa:8c:0a (Unknown)
 192.168.1.129 14:10:9f:d2:23:57 (Unknown)
 192.168.1.126 e4:f8:9c:91:14:73 (Unknown)
 192.168.1.214 00:26:ab:61:a3:b3 SEIKO EPSON CORPORATION
 192.168.1.127 28:e1:4c:9c:89:4f (Unknown)
 192.168.1.125 f4:e3:fb:dc:19:08 (Unknown)
 192.168.1.132 60:92:17:71:f4:6d (Unknown)
 
 24 packets received by filter, 0 packets dropped by kernel
 Ending arp-scan 1.8.1: 256 hosts scanned in 7.327 seconds (34.94 hosts/sec). 15 responded
把/usr/share/arp-scan裡面的mac-vendor.txt替換成更完整的版本,就能讓相同的掃描效果更好。Wireshark有提供一套相當好的 NIC供應商代碼清單。可惜檔案格式和arp-scan不相容,或者至少要做修改。但在用好幾個正規表達式來改寫檔案後,就能把資料變成arp-scan能接受的格式。你運氣很好,我已經把麻煩的部份都做好,並且發表成Gist了
你可以直接依以下做法更新mac-vendor.txt檔案:
 $ cd /usr/share/arp-scan
 $ sudo mv mac-vendor.txt mac-vendor.orig
 $ sudo wget http://bit.ly/mac-vendor

現在網路掃描產生的結果就更有趣了:
 $ sudo arp-scan –retry=8 –ignoredups -I wlan0 –localnet
 Interface: wlan0, datalink type: EN10MB (Ethernet)
 Starting arp-scan 1.8.1 with 256 hosts (http://www.nta-monitor.com/tools/arp-scan/)
 192.168.1.208 70:73:cb:b2:91:ee Apple, Inc.
 192.168.1.86 00:e0:4c:c6:86:01 REALTEK SEMICONDUCTOR CORP.
 192.168.1.253 9e:97:26:94:76:e4 (Unknown)
 192.168.1.254 9c:97:26:94:76:e4 Technicolor
 192.168.1.121 60:03:08:aa:8c:0a Apple, Inc.
 192.168.1.93 28:cf:e9:57:cd:01 Apple, Inc.
 192.168.1.125 f4:e3:fb:dc:19:08 HUAWEI TECHNOLOGIES CO.,LTD
 192.168.1.133 24:4b:81:3d:e9:ad Samsung Electronics Co.,Ltd
 192.168.1.137 ac:e0:10:c7:c5:67 Liteon Technology Corporation
 192.168.1.130 7c:c3:a1:b1:42:52 Apple, Inc.
 192.168.1.129 14:10:9f:d2:23:57 Apple, Inc.
 192.168.1.131 f4:5c:89:8b:79:a7 Apple, Inc.
 192.168.1.126 e4:f8:9c:91:14:73 Intel Corporate
 192.168.1.135 08:6d:41:bf:ff:1a Apple, Inc.
 192.168.1.134 b4:8b:19:2c:5f:de Apple, Inc.
 192.168.1.214 00:26:ab:61:a3:b3 SEIKO EPSON CORPORATION
 192.168.1.120 e0:ac:cb:a3:1d:04 Apple, Inc.
 192.168.1.127 28:e1:4c:9c:89:4f Apple, Inc.
 192.168.1.132 60:92:17:71:f4:6d Apple, Inc.
 
 33 packets received by filter, 0 packets dropped by kernel
 Ending arp-scan 1.8.1: 256 hosts scanned in 7.927 seconds (32.29 hosts/sec). 19 responded

裝置計數

arp-scan相對快速,一般可以在遠低於nmap的時間內回報結果。它在尋找網路上裝置方面也相當可靠,所以我們可以用它來記錄一天下來連線我們網路的裝置數目和身份。

最簡單的做法是用arp-scan來定時計算裝置數,並記錄在資料庫。這樣可以回報當前的數字,也可以事後分析資料。我寫了簡單的Perl程式碼來執行這個功能,但要先安裝幾個工具才能使用。現在安裝以下的套件:
 $ sudo apt-get install dnsutils
 $ sudo apt-get install libdbd-sqlite3-perl
 $ sudo apt-get install libgetopt-long-descriptive-perl
 $ sudo apt-get install libdatetime-format-iso8601-perl

接著從Github下載Perl程式碼,儲存到Raspberry Pi。
 #!/usr/bin/env perl
 
 use strict;
 use warnings;
 
 use DBI;
 use Getopt::Long;
 use DateTime;
 
 my ( %opt );
 my $status = GetOptions(“network=s” => $opt{“network”},
 ”dig” => $opt{“dig”});
 $opt{“network”} = ”network” unless defined $opt{“network”};
 
 print ”nSCANNINGn——–n”;
 
 my ($stmt, $sth, $rv);
 
 my $dbfile = ”/home/pi/$opt{‘network’}.db”;
 my $dbh = DBI->connect(“dbi:SQLite:dbname=$dbfile”,””,””);
 
 my $scan = `arp-scan –retry=8 –ignoredups -I wlan0 –localnet`;
 my @lines = split(“n”, $scan);
 
 my $csv = undef;
 my $count = 0;
 foreach my $line (@lines) {
 chomp($line);
 if ($line =~ /^s*((?:d{1,3}.){3}d{1,3})s+((?:[a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2})s+(S.*)/) {
 my $ip = $1;
 my $mac = $2;
 my $desc = $3;
 print ”IP=$ip, MAC=$mac, DESC=$desc”;
 
 # Dig for the mDNS name associated with the IP
 my $mdns = undef;
 if (defined($opt{“dig”})) {
 
 $stmt = qq(CREATE TABLE IF NOT EXISTS mdns(mac TEXT NOT NULL PRIMARY KEY UNIQUE, mdns TEXT););
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 $sth->finish();
 
 my $dig = `dig -x ${ip} @224.0.0.251 -p 5353`;
 #print $dig;
 
 my @report = split(“n”, $dig);
 my $answer = 0;
 my $local = undef;
 foreach my $entry (@report) {
 chomp($entry);
 if ( $answer == 1 ) {
 $local = $entry;
 last;
 }
 if( $entry eq ”;; ANSWER SECTION:”) {
 $answer = 1;
 }
 
 }
 if ( defined $local ) {
 #print ”local name = $localn”;
 ( $mdns ) = ($local =~ /s+(S+.local).$/);
 print ”, LOCAL=$mdnsn”;
 
 $stmt = qq(INSERT OR REPLACE INTO mdns (mac, mdns) VALUES (“$mac”,”$mdns”));
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 $sth->finish();
 
 } else {
 print ”n”;
 }
 } else {
 print ”n”;
 }
 
 $stmt = qq(CREATE TABLE IF NOT EXISTS macs(mac TEXT NOT NULL PRIMARY KEY UNIQUE, count INTEGER, description TEXT););
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 $sth->finish();
 
 $stmt = qq(SELECT count FROM macs WHERE mac=”$mac”;);
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 my @row = $sth->fetchrow_array();
 $sth->finish();
 
 my $previous;
 if (defined( $row[0] )) {
 $previous = $row[0]
 } else {
 $previous = 0;
 }
 print ”Previously seen ’$previous’ timesn”;
 
 $stmt = qq(INSERT OR REPLACE INTO macs (mac, count, description) VALUES (“$mac”,$previous+1,”$desc”));
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 $sth->finish();
 
 if (!defined $csv) {
 $csv = ”$mac”;
 } else {
 $csv = $csv . ”,$mac”;
 }
 $count++;
 }
 }
 
 my $time = DateTime->now->iso8601;
 print ”nRESULTn——n”;
 print ”time = $timen”;
 print ”count = $countn”;
 print ”csv = $csvn”;
 
 $stmt = qq(CREATE TABLE IF NOT EXISTS scan(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, datetime TEXT UNIQUE, count INTEGER, macs TEXT););
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 $sth->finish();
 
 $stmt = qq(INSERT INTO scan (datetime, count, macs) VALUES (“$time”,$count,”$csv”));
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 $sth->finish();
 
 $stmt = qq(CREATE TABLE IF NOT EXISTS days(date TEXT UNIQUE NOT NULL PRIMARY KEY, average INTEGER, samples TEXT););
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 $sth->finish();
 
 print ”nAVERAGEn——-n”;
 my $day = DateTime->now->ymd(‘-‘);
 print ”Today is $dayn”;
 $stmt = qq(SELECT samples FROM days WHERE date=”$day”;);
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 my @row = $sth->fetchrow_array();
 $sth->finish();
 
 my $samples;
 if (defined( $row[0] )) {
 $samples = $row[0];
 } else {
 $samples = undef;
 }
 my @values;
 if (!defined $samples) {
 $values[0] = $count;
 $samples = ”$count”;
 } else {
 @values = split(‘,’, $samples);
 push (@values, $count);
 $samples = $samples . ”,$count”;
 }
 
 print ”Samples = $samplesn”;
 #print ”values = @valuesn”;
 
 my $average = 0;
 foreach my $value (@values) {
 $average = $average + $value;
 }
 $average = int($average/scalar(@values));
 
 $stmt = qq(INSERT OR REPLACE INTO days (date, average, samples) VALUES (“$day”,$average,”$samples”));
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 $sth->finish();
 
 print ”Current running average for today is $average devices on the network.n”;
 
 $dbh->disconnect();
 exit;
這個程式碼會在wlan0執行區域網路的ARP掃描,並把結果儲存到SQLite資料庫。資料庫的預設名稱是network.db,但可以用引數「–network NAME」在指令行傳遞資料庫名稱,其中NAME是程式碼會自動加上「.db」結尾的資料庫檔案名稱。

程式碼也能檢查裝置是否提供mDNS關聯轉送地址。傳遞指令行引數「–dig」就能啟用這個功能,但這樣會嚴重影響程式碼的效能,讓它速度下降很多。然而,因為我們要把掃描結果序列化到SQLite資料庫,所以其實只要偶爾執行這個程式碼來填入主機的轉送地址。

資料庫有四個表格。第一個表格名為scan,記錄每次ARP掃描的時間和出現的主機。

 CREATE TABLE scan(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, datetime TEXT UNIQUE, count INTEGER, macs TEXT)

下一個表格名為macs,記錄每個不同的MAC位址出現在ARP掃描的次數,以及NIC的供應商(如果可得知)。
 CREATE TABLE macs(mac TEXT NOT NULL PRIMARY KEY UNIQUE, count INTEGER, description TEXT)

第三個表格名為days,記錄單日每次掃描出現的主機數,以及計算當日連線到網路的「平均裝置數」。
 CREATE TABLE days(date TEXT UNIQUE NOT NULL PRIMARY KEY, average INTEGER, samples TEXT)

最後一個表格名為mdns,在程式碼以–dig指令行引數執行時才建立。這個表格儲存MAC位址和裝置裝置宣告出來的mDNS轉送位址的對應。
 CREATE TABLE mdns(mac TEXT NOT NULL PRIMARY KEY UNIQUE, mdns TEXT)
我們可以定時執行這個程式碼,例如每半小時一次,並選擇每天一兩次使用較慢的「–dig」指令行引數來填入「mdns」表格,對應裝置的MAC位址和mDNS的轉送位址。

這麼做之前,先從指令行執行程式碼來測試
 $ sudo ./counter.pl –network home

或者如果你耐心夠,可以搜尋主機的mDNS轉送位址:
 $ sudo ./counter.pl –network home –dig

如此會建立一個名為「home.db」的資料庫。執行程式碼幾次後,用你喜歡的資料庫檢查器應用程式看看資料庫。

可以發現可見的主機總數稍有變化,因為有時候ARP掃描會遺漏幾台主機。你可以讓程式碼更可靠,把重試次數「–retry=8」提高。然而,數字越高,ARP掃描就越慢。

加入Crontab

一切就緒後,現在把crontab檔案加入程式碼。我們會設定讓它執行「定時」掃描,每半小時一次。
需要以根執行程式碼,如下:
 $ sudo su
 $ crontab -e

接著修改根使用者crontab檔案如下:
 0,30 * * * * /home/pi/counter.pl –network home

你還可以加入第二個條目來偶爾執行「強化」掃描,並啟用「–dig」來嘗試找出每台發現的主機的mDNS轉送位址。如果你對contab還不熟,想要先練習,在Raspberry Pi網站有不錯的教學

啟動

我們還可以把程式碼加入/etc/rc.local檔案,確保每次Raspberry Pi重新啟動時都會更新資料庫:
 #!/bin/sh -e
 #
 # rc.local
 
 # su pi -c ’/usr/local/bin/kismet_server -n -c mon1 –daemonize’
 /home/pi/counter.pl –network kaleider &
 
 exit 0

打造巨大顯示器

前面都還沒什麼硬體的改造,但接下來就不一樣了。讓Perl程式碼每半小時執行後,我們就有網路上有多少裝置的即時數字。
圖片

Luke Arztz攝

整個專案裡唯一需要焊接的地方是把驅動器板連接到巨型七段顯示器板後面。Sparkfun已經提供了很深入的接線指南,教你做每個步驟,以及焊接齒形安裝孔的精闢建議來幫助不熟悉的人。
圖片

Luke Arztz攝

依照Sparkfun的說明把驅動器連接到兩個七段顯示器。接著拿出Arduino Uno並把兩個面板接起來。
圖片

Luke Arztz攝

Arduino和筆記型電腦透過USB線連接的同時,無法為顯示器供電,這時12V電源就派上用場,記得把它插進板子上的筒形插孔。這時候,你會有一條USB線(接到筆電)和一條從Arduino接出來的電源線。
圖片

Luke Arztz攝

Sparkfun的接線指南中有很棒的程式碼範例。接線就緒後,載入到Arduino,測試焊接的成果。
 /*
 Controlling large 7-segment displays
 By: Nathan Seidle
 SparkFun Electronics
 Date: February 25th, 2015
 License: This code is public domain but you buy me a beer if you use this and we meet someday (Beerware license).
 
 This code demonstrates how to post two numbers to a 2-digit display usings two large digit driver boards.
 
 Here’s how to hook up the Arduino pins to the Large Digit Driver IN
 
 Arduino pin 6 -> CLK (Green on the 6-pin cable)
 5 -> LAT (Blue)
 7 -> SER on the IN side (Yellow)
 5V -> 5V (Orange)
 Power Arduino with 12V and connect to Vin -> 12V (Red)
 GND -> GND (Black)
Large Digit Driver有兩個連接器。「IN」是輸入側,要連接到微控制器(Arduino)。「OUT」是輸出側,要連接到額外數字的「IN」。一台顯示器在所有區段和小數點亮起時會用約150mA的電流。
*/
 
 //GPIO宣告
 //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
 byte segmentClock = 6;
 byte segmentLatch = 5;
 byte segmentData = 7;
 
 //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
 
 void setup()
 {
 Serial.begin(9600);
 Serial.println(“Large Digit Driver Example”);
 
 pinMode(segmentClock, OUTPUT);
 pinMode(segmentData, OUTPUT);
 pinMode(segmentLatch, OUTPUT);
 
 digitalWrite(segmentClock, LOW);
 digitalWrite(segmentData, LOW);
 digitalWrite(segmentLatch, LOW);
 }
 
 int number = 0;
 
 void loop()
 {
 showNumber(number); //Test pattern
 number++;
 number %= 100; //Reset x after 99
 
 Serial.println(number); //For debugging
 
 delay(500);
 }
 
 //拿一個數字,顯示2個數字。顯示絕對值(無負值)
 void showNumber(float value)
 {
 int number = abs(value); //移除負號和任何小數點
 
 //Serial.print(“number: ”);
 //Serial.println(number);
 
 for (byte x = 0 ; x < 2 ; x++)
 {
 int remainder = number % 10;
 
 postNumber(remainder, false);
 
 number /= 10;
 }
 
 //鎖定目前區段資料
 digitalWrite(segmentLatch, LOW);
 digitalWrite(segmentLatch, HIGH); //暫存器移動RCK上升邊緣的儲存暫存器
 }
 
 //給予數字或「-」,轉移到顯示器
 void postNumber(byte number, boolean decimal)
 {
 // - A
 // / / F/B
 // - G
 // / / E/C
 // -. D/DP
 
 #define a 1<<0
 #define b 1<<6
 #define c 1<<5
 #define d 1<<4
 #define e 1<<3
 #define f 1<<1
 #define g 1<<2
 #define dp 1<<7
 
 byte segments;
 
 switch (number)
 {
 case 1: segments = b | c; break;
 case 2: segments = a | b | d | e | g; break;
 case 3: segments = a | b | c | d | g; break;
 case 4: segments = f | g | b | c; break;
 case 5: segments = a | f | g | c | d; break;
 case 6: segments = a | f | g | e | c | d; break;
 case 7: segments = a | b | c; break;
 case 8: segments = a | b | c | d | e | f | g; break;
 case 9: segments = a | b | c | d | f | g; break;
 case 0: segments = a | b | c | d | e | f; break;
 case ’ ’: segments = 0; break;
 case ’c’: segments = g | e | d; break;
 case ’-‘: segments = g; break;
 }
 
 if (decimal) segments |= dp;
 
 //將這些位元時鐘輸出到驅動器
 for (byte x = 0 ; x < 8 ; x++)
 {
 digitalWrite(segmentClock, LOW);
 digitalWrite(segmentData, segments & 1 << (7 - x));
 digitalWrite(segmentClock, HIGH); //資料轉移到SRCK上升邊緣的暫存器
 }
 }

Arduino程式編寫

各元件都能獨立運作後,我們可以拿Nathan的程式碼範例來修改,讓Arduino在序列埠接受數字,並用相連的兩臺七段顯示器來顯示數字。接著把Arduino接到Raspberry Pi,並修改原本的掃描程式碼來推送當前的裝置數到序列埠。

Github可以下載修改後的Arduino腳本程式碼。從程式碼可以看到我把最後傳遞到程式碼的數字儲存在EEPROM,這代表當我們開啟和關閉Raspberry Pi的序列連線時,顯示器不會在Arduino重設時空白,因為Perl程式碼會開啟序列連線。
 #include
 
 byte segmentClock = 6;
 byte segmentLatch = 5;
 byte segmentData = 7;
 String readString;
 
 int addr = 0;
 
 void setup() {
 Serial.begin(9600);
 Serial.println(“Network Counter”);
 
 pinMode(segmentClock, OUTPUT);
 pinMode(segmentData, OUTPUT);
 pinMode(segmentLatch, OUTPUT);
 
 digitalWrite(segmentClock, LOW);
 digitalWrite(segmentData, LOW);
 digitalWrite(segmentLatch, LOW);
 
 int n = EEPROM.read(addr);
 showNumber(n);
 
 }
 
 void loop() {
 while (Serial.available()) {
 char c = Serial.read();
 readString += c;
 delay(2);
 }
 if (readString.length() >0) {
 Serial.println(readString);
 int n = readString.toInt();
 showNumber(n);
 EEPROM.update(addr, n);
 readString = ””;
 }
 }
 
 void showNumber(float value) {
 int number = abs(value);
 
 for (byte x = 0 ; x < 2 ; x++) {
 int remainder = number % 10;
 postNumber(remainder, false);
 number /= 10;
 }
 
 digitalWrite(segmentLatch, LOW);
 digitalWrite(segmentLatch, HIGH);
 }
 
 void postNumber(byte number, boolean decimal) {
 
 #define a 1<<0
 #define b 1<<6
 #define c 1<<5
 #define d 1<<4
 #define e 1<<3
 #define f 1<<1
 #define g 1<<2
 #define dp 1<<7
 
 byte segments;
 
 switch (number) {
 case 1: segments = b | c; break;
 case 2: segments = a | b | d | e | g; break;
 case 3: segments = a | b | c | d | g; break;
 case 4: segments = f | g | b | c; break;
 case 5: segments = a | f | g | c | d; break;
 case 6: segments = a | f | g | e | c | d; break;
 case 7: segments = a | b | c; break;
 case 8: segments = a | b | c | d | e | f | g; break;
 case 9: segments = a | b | c | d | f | g; break;
 case 0: segments = a | b | c | d | e | f; break;
 case ’ ’: segments = 0; break;
 case ’c’: segments = g | e | d; break;
 case ’-‘: segments = g; break;
 }
 
 if (decimal) segments |= dp;
 
 
 
 for (byte x = 0 ; x < 8 ; x++) {
 
   digitalWrite(segmentClock, LOW);
 
   digitalWrite(segmentData, segments & 1 << (7 - x));
 
   digitalWrite(segmentClock, HIGH);
 
  }
 
 }
把修改後的腳本程式碼上傳到Arduino,並開啟Arduino序列主控台,確定主控台設為9,600鮑,並輸入一個數字並傳送到Arduino。順利的話,巨型七段顯示器上的數字會改變。

Arduino與Raspberry Pi連接

現在來把所有元件整合在一起。把Arduino的USB線從筆電拔除,改插Raspberry Pi。順利的話,插入後馬上就會顯示為序列裝置。
 $ ls /dev/tty*
 /dev/tty   /dev/tty19 /dev/tty3   /dev/tty40 /dev/tty51 /dev/tty62
 /dev/tty0   /dev/tty2   /dev/tty30 /dev/tty41 /dev/tty52 /dev/tty63
 /dev/tty1   /dev/tty20 /dev/tty31 /dev/tty42 /dev/tty53 /dev/tty7
 /dev/tty10 /dev/tty21 /dev/tty32 /dev/tty43 /dev/tty54 /dev/tty8
 /dev/tty11 /dev/tty22 /dev/tty33 /dev/tty44 /dev/tty55 /dev/tty9
 /dev/tty12 /dev/tty23 /dev/tty34 /dev/tty45 /dev/tty56 /dev/ttyAMA0
 /dev/tty13 /dev/tty24 /dev/tty35 /dev/tty46 /dev/tty57 /dev/ttyS0
 /dev/tty14 /dev/tty25 /dev/tty36 /dev/tty47 /dev/tty58 /dev/ttyUSB0
 /dev/tty15 /dev/tty26 /dev/tty37 /dev/tty48 /dev/tty59 /dev/ttyprintk
 /dev/tty16 /dev/tty27 /dev/tty38 /dev/tty49 /dev/tty6
 /dev/tty17 /dev/tty28 /dev/tty39 /dev/tty5   /dev/tty60
 /dev/tty18 /dev/tty29 /dev/tty4   /dev/tty50 /dev/tty61
這裡可以看到它在Pi上顯示為/dev/ttyUSB0。現在安裝以下的套件:
 $ sudo apt-get install libdevice-serialport-perl

接著從Github下載更新的Perl程式碼,儲存到Raspberry Pi,取代先前的counter.pl程式碼。依照你自己的情形修改/dev/ttyUSB0這個部份。
 #!/usr/bin/env perl
 
 
 #+
 
 # Name:
 #   counter.pl
 
 # Language:
 #   Perl
 
 # Purpose:
 #   Count the number of devices on a network
 
 # Description
 #   Script uses an ARP scan to counter the number of devices on the network 
 #   and saves this information to an SQLite3 database. Database will be created
 #   the first time the script is run.
 
 # External Modules:
 #   Getopt::Long
 #   DBI
 #   DataTime
 #   Device::SerialPort
 
 # Authors:
 #   ANA: Alasdair Allan (Babilim Light Industries)
 
 # History
 #   06-FEB-2017 (ANA):
 #     Checking for duplicate MAC address with multiple IP. Assuming it’s a single device.  
 
 #   04-FEB-2017 (ANA):
 #     Added serial output to Arduino.
 #   03-FEB-2017 (ANA):
 #    Added optional lookup of mDNS forward address of scanned hosts.
 #   02-JAN-2017 (AA):
 #     Original Version
 
 # Copyright:
 #   Copyright (C) 2017. Babilim Light Industries, Ltd. Released under the MIT License.
 
 #-
 
 # L O A D   M O D U L E S ———————————————–
 
 use strict;
 use warnings;
 
 use DBI;
 use Getopt::Long;
 use DateTime;
 use Device::SerialPort;
 
 # O P T I O N S   H A N D L I N G —————————————
 
 my ( %opt );
 my $status = GetOptions(“network=s” => $opt{“network”},
                        ”dig”       => $opt{“dig”});
 $opt{“network”} = ”network” unless defined $opt{“network”};
 
 # S E R I A L P O R T ————————————————–
 
 my $port = Device::SerialPort->new(“/dev/ttyUSB0”);
 $port->baudrate(9600);
 $port->databits(8);
 $port->parity(“none”);
 $port->stopbits(1);
 
 # R U N S C A N ——————————————————-
 
 print ”nSCANNINGn——–n”;
 
 my ($stmt, $sth, $rv);
 
 my $dbfile = ”/home/pi/$opt{‘network’}.db”;
 my $dbh = DBI->connect(“dbi:SQLite:dbname=$dbfile”,””,””);
 
 my $scan = `arp-scan –retry=8 –ignoredups -I wlan0 –localnet`;
 my @lines = split(“n”, $scan);
 
 my $csv = undef;
 my $count = 0;
 foreach my $line (@lines) {
 chomp($line);
 if ($line =~ /^s*((?:d{1,3}.){3}d{1,3})s+((?:[a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2})s+(S.*)/) {
 my $ip = $1;
 my $mac = $2;
 my $desc = $3;
 print ”IP=$ip, MAC=$mac, DESC=$desc”;
 
 # Dig for the mDNS name associated with the IP
 my $mdns = undef;
 if (defined($opt{“dig”})) {
 
 $stmt = qq(CREATE TABLE IF NOT EXISTS mdns(mac TEXT NOT NULL PRIMARY KEY UNIQUE, mdns TEXT););
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 $sth->finish();
 
 my $dig = `dig -x ${ip} @224.0.0.251 -p 5353`;
 #print $dig;
 
 my @report = split(“n”, $dig);
 my $answer = 0;
 my $local = undef;
 foreach my $entry (@report) {
 chomp($entry);
 if ( $answer == 1 ) {
 $local = $entry;
 last;
 }
 if( $entry eq ”;; ANSWER SECTION:”) {
 $answer = 1;
 }
 
 }
 if ( defined $local ) {
 #print ”local name = $localn”;
 ( $mdns ) = ($local =~ /s+(S+.local).$/);
 print ”, LOCAL=$mdnsn”;
 
 $stmt = qq(INSERT OR REPLACE INTO mdns (mac, mdns) VALUES (“$mac”,”$mdns”));
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 $sth->finish();
 
 } else {
 print ”n”;
 }
 } else {
 print ”n”;
 }
 
 my $duplicate = undef;
 if(defined $csv) {
 if( $csv =~ /$mac/ ) {
 $duplicate = 1;
 }
 }
 
 if ( !$duplicate ) {
 $stmt = qq(CREATE TABLE IF NOT EXISTS macs(mac TEXT NOT NULL PRIMARY KEY UNIQUE, count INTEGER, description TEXT););
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 $sth->finish();
 
 $stmt = qq(SELECT count FROM macs WHERE mac=”$mac”;);
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 my @row = $sth->fetchrow_array();
 $sth->finish();
 
 my $previous;
 if (defined( $row[0] )) {
 $previous = $row[0]
 } else {
 $previous = 0;
 }
 print ”Previously seen ’$previous’ timesn”;
 
 $stmt = qq(INSERT OR REPLACE INTO macs (mac, count, description) VALUES (“$mac”,$previous+1,”$desc”));
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 $sth->finish();
 
 if (!defined $csv) {
 $csv = ”$mac”;
 } else {
 $csv = $csv . ”,$mac”;
 }
 $count++;
 } else {
 print ”Ignoring duplicate MAC ’$mac’n”;
 }
 
 }
 }
 
 my $time = DateTime->now->iso8601;
 print ”nRESULTn——n”;
 print ”time = $timen”;
 print ”count = $countn”;
 print ”csv = $csvn”;
 
 # S E R I A L P O R T ————————————————–
 
 $port->write($count);
 
 # S A V E S C A N T O D A T A B A S E ———————————-
 
 $stmt = qq(CREATE TABLE IF NOT EXISTS scan(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, datetime TEXT UNIQUE, count INTEGER, macs TEXT););
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 $sth->finish();
 
 $stmt = qq(INSERT INTO scan (datetime, count, macs) VALUES (“$time”,$count,”$csv”));
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 $sth->finish();
 
 # L A S T O R D E R S ————————————————-
 
 $stmt = qq(CREATE TABLE IF NOT EXISTS days(date TEXT UNIQUE NOT NULL PRIMARY KEY, average INTEGER, samples TEXT););
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 $sth->finish();
 
 print ”nAVERAGEn——-n”;
 my $day = DateTime->now->ymd(‘-‘);
 print ”Today is $dayn”;
 $stmt = qq(SELECT samples FROM days WHERE date=”$day”;);
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 my @row = $sth->fetchrow_array();
 $sth->finish();
 
 my $samples;
 if (defined( $row[0] )) {
 $samples = $row[0];
 } else {
 $samples = undef;
 }
 my @values;
 if (!defined $samples) {
 $values[0] = $count;
 $samples = ”$count”;
 } else {
 @values = split(‘,’, $samples);
 push (@values, $count);
 $samples = $samples . ”,$count”;
 }
 
 print ”Samples = $samplesn”;
 #print ”values = @valuesn”;
 
 my $average = 0;
 foreach my $value (@values) {
 $average = $average + $value;
 }
 $average = int($average/scalar(@values));
 
 $stmt = qq(INSERT OR REPLACE INTO days (date, average, samples) VALUES (“$day”,$average,”$samples”));
 $sth = $dbh->prepare( $stmt );
 $rv = $sth->execute();
 $sth->finish();
 
 print ”Current running average for today is $average devices on the network.n”;
 
 $port->close();
 $dbh->disconnect();
 exit;
 
 # ———————————————————————–
這個程式碼和我們原本的counter.pl程式碼很像,但它用序列資料庫來連接Arduino,並傳送當前的裝置數到顯示器。現在照常執行程式碼。
這就完成了。你擁有一台設定完成、運作順利且高調明顯的網路計數器。

動手做更多

這個專題還有很大的發展空間,可以增加掃描器記錄的資訊,或是加裝更多顯示器。
舉例來說,蘇黎世ETH Entrepreneur Club的玩家們設置讓Raspberry Pi擷取無線封包並計算數量,接著對應流量,分為0到10的等級,並以彩虹色的LED長條圖來顯示,簡單呈現網路目前的負載程度。你可以考慮用巨型LED長條圖來顯示在mon1介面監控的TCP流量。

另一個選擇是「中斷網路」的緊急按鈕,用在覺得事有蹊翹的時候。太多可能性,時間都不夠用了。

(譯:屠建明)
原文

延伸閱讀
專題:Raspberry Pi跳舞布偶佩佩
用Raspberry Pi打造自動化海水缸
將老舊攜帶型電視改造成Raspberry Pi電子遊樂器
小如iPhone的攜帶型Linux終端機
最新Raspberry Pi Zero W只要10美元就有內建Wi-Fi及藍牙功能

Social media & sharing icons powered by UltimatelySocial