Amazon Dash ButtonでRouter Crash Buttonを作ってみた with Ruby

この記事は、[NetOpsCoding Advent Calendar 2016](http://qiita.com/advent-calendar/2016/netopscoding)の2016年12月17日の記事です。

「[Amazon Dash Button](https://www.amazon.co.jp/b?ie=UTF8&node=4752863051)」を入手したので、ルータ系で何かできないかと考えた結果、ルータをクラッシュさせる「Router Crash Button」を作ってみました。ボタンを押すと、ルータがクラッシュします。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Amazon Dash Buttonで、ルータをCrashさせてみた。 <a href="https://t.co/iZR7vj6ToR">pic.twitter.com/iZR7vj6ToR</a></p>&mdash; kooshin (@kooshin) <a href="https://twitter.com/kooshin/status/806875235408453632">2016年12月8日</a></blockquote>


## 仕組み
ボタンを押してからルータがクラッシュまで、下記の通りで実行します。

1. 人がボタンを押す
2. スクリプトがボタンが押されたことを検知（ARPパケット検知）
3. スクリプトがルータにTelnetする
4. スクリプトがルータの隠しコマンド（[test crash](http://www.cisco.com/cisco/web/support/JP/100/1001/1001516_gsr_coredump.html) ）を発行し、Bus Errorを強制的に発生
5. ルータがクラッシュする
6. ルータが再起動する
7. また、人がボタンを押す。。。。
8. 2に戻る


### Amazon Dash Button
Amazon Dash Buttonは、インターネットと通信するために、IPアドレスがDHCPから割り当てられます。IPアドレスが割り当てられる機器のため、ボタンごとに、MACアドレスが異なります。このMACアドレスを利用して、ボタンを識別します。今回は「消臭力」のAmazon Dash Buttonを入手しました。設定方法は参考文献を参照してください。
![Amazon Dash Button.png](https://qiita-image-store.s3.amazonaws.com/0/12026/d952acdb-f5a7-74ae-2634-64f8936e031e.png)


### ボタンが押されたことの検知（ARPパケット検知）
Amazon Dash Buttonが押された場合、ARPパケットがブロードキャストされます。このARPパケットをパケットキャプチャすることで、ボタンが押されたことを検知します。
Amazon Dash Buttonは、電源OFFの状態で、ボタンが押されると電源ONし、DHCPでIPアドレスの割当が行われた後、IPアドレスの重複確認のために、ARP Requestパケットが送信されます。このARP Requestパケットの送信元MACアドレスを確認し、Amazon Dash ButtonのMACアドレスと比較して、合致していればボタンが押されたと判断します。


## 環境
無線LAN環境で事前にAmazon Dash Buttonの設定をします。設定方法は参考文献を参照してください。

- Ubuntu Linux 16.04
- libpcap0.8 - パケットキャプチャ用
- Ruby 2.3.0
    - expect4r 0.0.11 - Telnetライブラリ
    - pcaprub 0.12.4 - パケットキャプチャライブラリ
    - pio 0.30.0 - パケットパースライブラリ

## コード
コードはgithubにありますので参照してください。
https://github.com/kooshin/router-crash-button

スクリプトの配置は下記のとおりです。

```
.
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── README.md
├── crash_button.rb     - Ruby実行ファイル（root権限で）
└── lib
    └── dash_button.rb  - Amazon Dash Button用のライブラリ
```


### crash_button.rb
Amazon Dash Buttonが押されたらルータをクラッシュさせるスクリプトです。

下記の通り、パラメータを設定します。デモコードのため、すべてスクリプトに埋め込んであります。

```rb
DASH_BUTTON_MAC_ADDR  = '88:71:e5:63:aa:d5' # ボタンのMACアドレス
NETDEV                = 'enp0s25'           # パケットキャプチャするインタフェース

ROUTER_HOST           = '192.168.88.5'      # クラッシュさせるルータのIPアドレス
ROUTER_USER           = 'cisco'             # Telnet時のユーザ名
ROUTER_PASSWORD       = 'cisco'             # Telnet時のパスワード
ROUTER_ENABLE         = 'cisco'             # enableパスワード
```

自作DashButtonクラス（lib/dash_button.rb）のインスタンスを作成します。引数はパケットキャプチャするインタフェースと、検知するAmazon Dash ButtonのMACアドレスです。MACアドレスは、Wireshark等でパケットキャプチャすることでわかります。

```rb
dash_button = DashButton.new(NETDEV, DASH_BUTTON_MAC_ADDR)
```

DashButtonクラスのmonitorメソッドにブロックを渡します。Amazon Dash Buttonを検知するとブロックが実行されます。
今回は、ルータをクラッシュさせるコードを実行します。expect4rライブラリを使ってTelnetしています。詳細は[Rubyのexpect4rでCiscoルータにTelnet/SSHしてコマンド実行する](http://qiita.com/kooshin/items/27f2b9ed5c8896279bcf)を参照してください。
ルータにログイン後、ios.exp_printで[test crash](http://www.cisco.com/cisco/web/support/JP/100/1001/1001516_gsr_coredump.html)コマンドを送信します。「Enable crash router selection marked with (crash router)」するために「C」を送信し、「(crash router) Bus Error, due to invalid address access」を引き起こすために、1を送信します。

```rb
dash_button.monitor do
  begin
    puts "#{DateTime.now} Crash Button pressed!! Force crash the Router"
    ios = Expect4r::Ios.new_telnet(
      host: ROUTER_HOST,
      user: ROUTER_USER,
      pwd:  ROUTER_PASSWORD,
      enable_password: ROUTER_ENABLE
    )
    ios.login
    ios.exp_print("test crash\rC\r1\r")
  rescue Expect4r::ExpTimeoutError, Errno::EIO => e
    puts "#{DateTime.now} Failed: #{e}"
  end
end
```

### lib/dash_button.rb
pcaprubライブラリとpioライブラリを使って、Amazon Dash ButtonのARP Requestパケットを検出します。

PCAPRUBY::Pcap.open_liveでパケットキャプチャを開始します。この際、インタフェースの指定と、BPFフィルタでARPパケットのみパケットキャプチャするよう指定します。

```rb
  def pcap
    return @pcap if @pcap

    @pcap = PCAPRUB::Pcap.open_live(
      netdev,
      SNAPLENGTH,
      PROMISCOUS_MODE,
      TIMEOUT
    )
    @pcap.setfilter(BPF_FILTER)
  end
```

monitorメソッド内のpcap.each_packetでパケットを受信するたびに、ボタンが押されたかを判断します。パケットをPio::Parser.readでパースします。パースしたパケットを、dash_button_pressed?メソッドで、ボタンが押されたかどうかを判断します。ボタンが押されたと判断した場合、monitorメソッドの呼び出し元のブロックが実行されます。

```rb
  def monitor
    pcap.each_packet do |raw_packet|
      begin
        packet = Pio::Parser.read(raw_packet.data)
        yield if dash_button_pressed?(packet)
      rescue Pio::ParseError
        next
      end
    end
  end
```

dash_button_pressed?メソッド内では、ARP Requestパケットであるか、指定のMACアドレスであるかを判断します。

```rb
  def dash_button_pressed?(packet)
    arp_packet?(packet) && dash_button_mac_addr?(packet)
  end

  def arp_packet?(packet)
    Pio::Arp::Request == packet.class
  end

  def dash_button_mac_addr?(packet)
    dash_button_mac_addr == packet.source_mac
  end
```


## 実行
パケットキャプチャのために、libpcap-devを予めインストールし、コードをcloneして、bundleコマンドでRubyのライブラリをインストールします。

```
$ sudo apt install libpcap-dev
$ git clone https://github.com/kooshin/router-crash-button
$ bundle
```

パケットキャプチャするため、スクリプトの実行にはroot権限が必要です。sudoで実行すると良いでしょう。説明は省略していますが、Rubyはrbenvを用いてインストールしています。rbenvの場合、別途[rbenv-sudo](https://github.com/dcarley/rbenv-sudo)プラグインが必要です。

```
$ sudo ruby crash_button.rb
or
$ rbenv sudo ruby crash_button.rb
Waiting for Crash Button press...
```

## 実行結果
上記のTwitterの動画を参照してください。


## 参考文献
* [Amazon Dash Button超速解説](http://qiita.com/ma2shita/items/b2fbaabf398db4324275)
* [Amazon Dash Buttonを(正しくない方向で)使ってみた](http://qiita.com/takustaqu/items/8539b33780c9675c8657)
* [Amazon Dash ButtonをただのIoTボタンとして使う](http://qiita.com/jsoizo/items/3b8bba4160f41aef20f4)