12月5日に日本でも Amazon Dash Button の提供が始まりましたが、すぐに下記の記事を投稿されている方がいて、面白そうだったので私もやってみました。
他にもすでに同様のことを行われている記事はあるのですが、パケットキャプチャ部分を Ruby で書いている記事は見つからなかったので、 Ruby で書いてみました。ボタンの設定は上記ページなどですでに詳しく紹介されていて、同様に行いましたのでここでは割愛します。
ちなみに本当は AWS IoT Button を使ってみたいのですが、まだ日本では発売されていないので、ひとまず Dash Button で我慢します。
処理の流れ
先ほどの記事でも紹介されていますが、 Dash Button が押された時の処理の流れは下記のようになります。
- 電源ON(通常時はOFFになっている)
- 設定されたWi-Fiネットワークに接続
- IP重複検知のために ARP Probe もしくは UDP Broadcast 送信
- Amazonへの購入処理実行
- 電源OFF
試しにボタンのIPアドレスにpingしてみたところ、ボタンを押すと電源が入ってpingが返ってくるようになり、処理が終わるとまた電源がOFFになってpingが返らなくなりました。
$ ping 192.168.10.10 PING 192.168.10.10 (192.168.10.10): 56 data bytes Request timeout for icmp_seq 0 Request timeout for icmp_seq 1 Request timeout for icmp_seq 2 Request timeout for icmp_seq 3 ping: sendto: No route to host Request timeout for icmp_seq 4 ping: sendto: Host is down Request timeout for icmp_seq 5 64 bytes from 192.168.10.10: icmp_seq=0 ttl=64 time=6763.680 ms 64 bytes from 192.168.10.10: icmp_seq=1 ttl=64 time=5758.602 ms 64 bytes from 192.168.10.10: icmp_seq=2 ttl=64 time=4753.781 ms 64 bytes from 192.168.10.10: icmp_seq=3 ttl=64 time=3752.679 ms 64 bytes from 192.168.10.10: icmp_seq=4 ttl=64 time=2749.437 ms 64 bytes from 192.168.10.10: icmp_seq=7 ttl=64 time=4.412 ms 64 bytes from 192.168.10.10: icmp_seq=8 ttl=64 time=6.484 ms 64 bytes from 192.168.10.10: icmp_seq=9 ttl=64 time=2.491 ms 64 bytes from 192.168.10.10: icmp_seq=10 ttl=64 time=3.172 ms Request timeout for icmp_seq 15 Request timeout for icmp_seq 16 Request timeout for icmp_seq 17 Request timeout for icmp_seq 18
Dash Button では AWS IoT Button のような設定の自由度はなく、購入処理のための通信を検知して処理を行う必要があります。そこで大きく分けて下記2つの内容を実装します。
Dash Button の ARP Probe もしくは UDP Broadcast をキャプチャしてMACアドレスを確認する
Dash Button のMACアドレスからの通信を検知したら目的の処理を行う
Dash Button のMACアドレス確認
Ruby でのパケットキャプチャ用ライブラリを探したところ、 PacketFu というライブラリがあったのでこちらを使用してみます。
実装コードは下記の通りです。
capture.rb
require 'packetfu' require './ouis.rb' include PacketFu def get_capture(iface) cap = Capture.new(iface: iface, start: true) cap.stream.each do |pkt| time = Time.now.strftime("%Y-%m-%d %H:%M:%S.%6N") if UDPPacket.can_parse?(pkt) udp_packet = UDPPacket.parse(pkt) src_ip = IPHeader.octet_array(udp_packet.ip_src).join('.') dst_ip = IPHeader.octet_array(udp_packet.ip_dst).join('.') src_port = udp_packet.udp_src dst_port = udp_packet.udp_dst src_mac, dst_mac, vendor_name = get_common_values(udp_packet) puts "time:#{time}, src_mac:#{src_mac}, dst_mac:#{dst_mac}, src_ip:#{src_ip}, dst_ip:#{dst_ip}, src_port:#{src_port}, dst_port:#{dst_port}, protocol:udp, vendor: #{vendor_name}" next end if ARPPacket.can_parse?(pkt) arp_packet = ARPPacket.parse(pkt) src_ip = arp_packet.arp_saddr_ip dst_ip = arp_packet.arp_daddr_ip src_mac, dst_mac, vendor_name = get_common_values(arp_packet) puts "time:#{time}, src_mac:#{src_mac}, dst_mac:#{dst_mac}, src_ip:#{src_ip}, dst_ip:#{dst_ip}, protocol:arp, vendor: #{vendor_name}" end end end def get_common_values(packet) src_mac = EthHeader.str2mac(packet.eth_src) dst_mac = EthHeader.str2mac(packet.eth_dst) vendor_name = get_vendor_name(src_mac) return src_mac, dst_mac, vendor_name end def get_vendor_name(mac) oui = mac.split(':').slice(0, 3).join('-') OUIS[oui.upcase] end if $0 == __FILE__ iface = ARGV[0] puts "Capturing for interface: #{iface}" get_capture(iface) end
キャプチャしたパケットが ARPパケットもしくはUDPパケットであれば、パケットを解析して必要な情報を抜き出して画面に出力しています。
また、Dash Button 以外からのパケットの情報も出力されるため、MACアドレスのベンダーコードからどのベンダーの端末からのパケットかも出すようにしています。あらかじめ IEEE のサイトからベンダーコードのリストを取得し、hash 形式で ouis.rb に保持し、 require して使っています。
IEEEのベンダーコードリスト(重いです)
http://standards.ieee.org/regauth/oui/oui_public.txt
ouis.rb
# http://standards.ieee.org/regauth/oui/oui_public.txt をもとに作成 OUIS = { 'E0-43-DB' => "Shenzhen ViewAt Technology Co.,Ltd. ", '24-05-F5' => "Integrated Device Technology (Malaysia) Sdn. Bhd.", '2C-30-33' => "NETGEAR", '3C-D9-2B' => "Hewlett Packard", '9C-8E-99' => "Hewlett Packard", 'B4-99-BA' => "Hewlett Packard", '1C-C1-DE' => "Hewlett Packard", '3C-35-56' => "Cognitec Systems GmbH", 〜〜〜以下略〜〜〜
そしてスクリプトを実行して Dash Button を押すと下記のような出力が確認できました。
$ sudo ruby capture.rb en0 | grep -i amazon /Users/akanuma/workspace/dash_button/ouis.rb:8296: warning: key "00-01-C8" is duplicated and overwritten on line 8309 /Users/akanuma/workspace/dash_button/ouis.rb:12779: warning: key "08-00-30" is duplicated and overwritten on line 17381 /Users/akanuma/workspace/dash_button/ouis.rb:12779: warning: key "08-00-30" is duplicated and overwritten on line 22049 time:2016-12-17 20:52:06.677957, src_mac:88:71:e5:f0:89:71, dst_mac:ff:ff:ff:ff:ff:ff, src_ip:0.0.0.0, dst_ip:255.255.255.255, src_port:68, dst_port:67, protocol:udp, vendor: Amazon Technologies Inc. time:2016-12-17 20:52:06.689735, src_mac:88:71:e5:f0:89:71, dst_mac:ff:ff:ff:ff:ff:ff, src_ip:192.168.10.10, dst_ip:192.168.10.1, protocol:arp, vendor: Amazon Technologies Inc.
ベンダーコードのリストで同一のベンダーコードが微妙に異なるベンダー名表記で複数回登録されているために warning が出ていますが、処理には影響ないので今回は無視します。ベンダー名が Amazon Technologies Inc. となっているのが Dash Button です。 src_mac として出力している内容が Dash Button のMACアドレスになりますので、このMACアドレスからの通信を検知して処理を行うスクリプトを実装します。
Dash Button からの通信を検知して Slack に投稿する
通信を検知するための処理は基本的にMACアドレスを確認するためのスクリプトと同様になります。パケットの送信元MACアドレスが Dash Button のものだったらSlackに投稿する処理を実行します。
post_to_slack.rb
require 'packetfu' require 'open3' require 'json' include PacketFu FILTER = nil MAC_OF_DASH = '88:71:e5:f0:89:71' SLACK_API_URL = 'Slack の Incoming Webhook URL' INTERVAL_MICRO_SECONDS = 2_000_000 @last_processed_time = 0 def get_capture(iface) cap = Capture.new(iface: iface, filter: FILTER, start: true) cap.stream.each do |pkt| next if !ARPPacket.can_parse?(pkt) && !UDPPacket.can_parse?(pkt) packet = Packet.parse(pkt) next if EthHeader.str2mac(packet.eth_src) != MAC_OF_DASH next if !past_since_last_processed? post_to_slack @last_processed_time = current_unixtime_with_micro_seconds t_stamp = Time.now.strftime("%Y-%m-%d %H:%M:%S.%6N") puts "#{t_stamp} Posted to Slack." end end def current_unixtime_with_micro_seconds now = Time.now "#{now.to_i}#{now.usec}".to_i end def past_since_last_processed? current_unixtime_with_micro_seconds - @last_processed_time >= INTERVAL_MICRO_SECONDS end def post_to_slack api_url = SLACK_API_URL payload = { channel: '#akanuma_private', username: 'dash', icon_emoji: ':squirrel:', text: 'Hello World from Dash Button!!' } command = "curl -X POST --data-urlencode 'payload=#{payload.to_json}' #{api_url}" puts command output, std_error, status = Open3.capture3(command) puts output puts std_error puts status end if $0 == __FILE__ iface = ARGV[0] puts "Capturing for interface: #{iface}" get_capture(iface) end
色々試してみたところ、 自宅ではボタンを押すと ARP と UDP のパケットが一度ずつ飛ぶのですが、会社のネットワークで試すと ARP や UDP のパケットが飛ぶタイミングを特定しきれず、ボタンを押した時に ARP が飛んだり飛ばなかったり、 UDP が複数回飛んだりするので、とりあえずの策として、ARPとUDP両方を検知して、前回の実行から2,000,000マイクロ秒(2秒)以内だったら実行しないようにしました。他の方の記事ではあまり回数がブレるというのはなさそうに見えるので、もう少し調べてみたいところです。この辺り詳しい方いたら教えていただけると嬉しいです。
Slackへの投稿は Open3 を使って Incoming Webhook のURLにポストしているだけのシンプルな内容です。これを実行すると下記のようにSlackに投稿されます。
今回はSlackに投稿しましたが、この内容さえ変えればボタンが押された時の処理は自由に変更できるので、アイディア次第で色々面白いことができそうに思っています。社内で使うちょっとしたツールなど色々試してみたいと思います。
LAN内にサーバが必要
今回はとりあえず動かしてみたということで、自分のMac上でスクリプトを動かしましたが、常時使うためのツールを作るのであれば、LAN内にサーバを用意する必要があります。これが AWS IoT Button であれば直接AWSのサービスと連携できると思うので、早く日本でも使えるようになってほしいですね。
Vagrant で稼働させたVM上で Dash Button のパケットをキャプチャできなかった
今回試す環境を最初は Vagrant でパブリックネットワーク設定のVMを作ってVM上で動かしていたのですが、VM上では Dash Button からのパケットが検知できませんでした。たまに検知できるときもあるんですが、検知できない場合が多いです。ただ、 冒頭で紹介した記事で紹介されている node.js でのやり方だとちゃんと検知できていますし、 tcpdump でパケットを見てみると、Macのローカル上でもVM上でも同様に検知できているので、PacketFuで解析している部分が何か違うのかなと思い色々調べてみましたが、特定できませんでした。VM上へはMacのネットワークインタフェースを経由して到達していると思うので、経由したIFの情報を送信元として認識しているような感じではあるのですが、この辺りも何かわかる方いたら情報いただけると嬉しいです。
今回のコード
今回のコードはこちらに公開しました。