IntelliJのremote interpreter方式を使って、Raspbian上のScapyをリモートデバッグしてみた

Raspberry Pi 2 Model BでScapyを書いていると、リモートデバッグしたくなりました。

そこで、IntelliJのremote interpreter方式を使って、Raspbian上のScapyをリモートデバッグしてみた時のメモを残します。

 
目次

 

環境

 

公開鍵を使ったSSHを可能にするよう設定

以前、

pi@raspberrypi:~ $ sudo raspi-config

から 5 Interfacing Options > P2 SSH にて enabled してありますので、その設定の続きです。

 
今回は、以下を参考に実施しました。
ツール・ラボ » Raspberry Piに公開鍵認証を使ってssh接続する

なお、今回はパスワード認証もとりあえず残しておきます。

以下ログです。  

# Macにてキーペアの作成
$ ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_raspberry_pi

# scpにてMacからラズパイへ公開鍵を転送
$ scp -P 22 ~/.ssh/id_rsa_raspberry_pi.pub pi@192.168.10.50:
pi@192.168.10.50's password: 
id_rsa_raspberry_pi.pub                        

# ラズパイにて転送されていることを確認
pi@raspberrypi:~ $ ls -al id*
-rw-r--r-- 1 pi pi 757 Jan  7 13:49 id_rsa_raspberry_pi.pub

# 鍵を移動して権限を設定
pi@raspberrypi:~ $ mkdir .ssh
pi@raspberrypi:~ $ cat id_rsa_raspberry_pi.pub >> .ssh/authorized_keys
pi@raspberrypi:~ $ chmod 700 .ssh
pi@raspberrypi:~ $ chmod 600 .ssh/authorized_keys

# 末尾に追加
pi@raspberrypi:~ $ sudo vi /etc/ssh/sshd_config
# 以下3行を追加
RSAAuthentication    yes
PubkeyAuthentication yes
AuthorizedKeysFile   %h/.ssh/authorized_keys

# 不要な公開鍵を削除してSSHを再起動
pi@raspberrypi:~ $ rm id_rsa_raspberry_pi.pub  
pi@raspberrypi:~ $ sudo /etc/init.d/ssh restart
[ ok ] Restarting ssh (via systemctl): ssh.service.

# Macより公開鍵認証でSSHできることを確認
$ ssh -i ~/.ssh/id_rsa_raspberry_pi pi@192.168.10.50
Linux raspberrypi 4.9.59-v7+ #1047 SMP Sun Oct 29 12:19:23 GMT 2017 armv7l
...
pi@raspberrypi:~ $ 

 

IntelliJを使ったリモートデバッグ設定

IntelliJのリモートデバッグには

の2つがあるようです。
PyCharmのリモートデバッグ機能を使ってみる - Qiita

 
今回は、以下を参考にSSHを使うremote interpreter方式を使ってみました。
PyCharmを使ってRaspberry Pi2上で快適リモートGPIOプログラミング - izm_11's blog

 
以下、作業ログです。

  • 既存のScapyプロジェクトを開く
  • Project Structureを開く
    • File > Project Structure
  • Python SDKをローカルから切り替える
    • Project > Project SDK > New > Python SDK > Add Remote
  • リモート設定は以下
項目 設定値
接続方式 SSH Credentials
Host 192.168.10.50 (必要に応じて差し替え)
User name pi
Auth type Key pair (OpenSSH or PuTTY)
Private key file /path/to/.ssh/id_rsa_raspberry_pi
Python interpreter path /usr/bin/python (デフォルト値)

 

  • パスマッピングを実施
    • メニューの Tools > Deployment > Configuration
    • 左上の + ボタン
    • 設定内容は以下
タブ 設定 項目値
Connection Name (任意の名前)
Visible only for this project チェックする
Type SFTP
SFTP Host 192.168.10.50
Port 22
Path /
User name pi
Auth type Key pair (OpenSSH or PuTTY)
Private key file /path/to/.ssh/id_rsa_raspberry_pi
Mappings Local path /path/to/マッピング
Deployment path on server /home/pi/scapy_sample

 

  • ファイルのアップロード
    • アップロードしたいファイルを開いた状態で、 Tools > Deployment > Upload to <サーバ名>
    • ディレクトリ階層があれば、その階層も含めてアップロードされる
  • アップロードの自動化
    • Tools > Deployment > Automatic Upload にチェックを入れる

 
以上で、Macとラズパイとでファイルの同期が取れるようになりました。

 

Scapyのための設定変更

今回は、以前使用したDHCPサーバを探すファイルをリモートデバッグしてみます。
https://github.com/thinkAmi-sandbox/scapy-sample/blob/master/dhcp/discover_dhcp_server.py

まず、ラズパイ上で実行するため、インタフェース名を変更しておきます。

USB_INTERFACE_NAME = 'eth0'

 
また、Scapyを実行するにはsudoする必要があります。ただ、通常のリモートデバッグではsudoした状態にはなりません。

そこで今回は、システムのPythonの権限まわりを変更しました。
Remote Debug GPIO on Raspberry Pi | Nathan Jones

あまり良くないのかもしれませんが、ラズパイということで今回は気にしないことにします。

pi@raspberrypi:~ $ sudo chown -v root:root /usr/bin/python
ownership of '/usr/bin/python' retained as root:root

pi@raspberrypi:~ $ sudo chmod -v u+s /usr/bin/python
mode of '/usr/bin/python' changed from 0755 (rwxr-xr-x) to 4755 (rwsr-xr-x)

 

リモートデバッグの実行

まずは、上記で変更を加えたファイルをラズパイへ転送します。

ブレークポイントを置いてデバッグ実行してみると、ブレークポイントで停止しました。

f:id:thinkAmi:20180110222702p:plain:w400

 
デバッグを続行すると、コンソールに以下が表示されました。

なお、DHCPサーバを探すためのパケット送信が止まらないので、 Cmd + F2 で止める必要があります。

ssh://pi@192.168.10.50:22/usr/bin/python -u /home/pi/.pycharm_helpers/pydev/pydevd.py --cmd-line --multiproc --qt-support=auto --client '0.0.0.0' --port 36797 --file /home/pi/scapy_sample/dhcp/discover_dhcp_server.py
pydev debugger: process 1661 is connecting

Connected to pydev debugger (build 173.4127.27)
###[ Ethernet ]### 
  dst       = FF:FF:FF:FF:FF:FF
  src       = 00:00:00:00:00:04
  type      = 0x800
###[ IP ]### 
     version   = 4
     ihl       = None
     tos       = 0x0
     len       = None
     id        = 1
     flags     = 
     frag      = 0
     ttl       = 64
     proto     = udp
     chksum    = None
     src       = 0.0.0.0
     dst       = 255.255.255.255
     \options   \
###[ UDP ]### 
        sport     = bootpc
        dport     = bootps
        len       = None
        chksum    = None
###[ BOOTP ]### 
           op        = BOOTREQUEST
           htype     = 1
           hlen      = 6
           hops      = 0
           xid       = 0
           secs      = 0
           flags     = 
           ciaddr    = 0.0.0.0
           yiaddr    = 0.0.0.0
           siaddr    = 0.0.0.0
           giaddr    = 0.0.0.0
           chaddr    = '\x00\x00\x00\x00\x00\x04'
           sname     = ''
           file      = ''
           options   = 'c\x82Sc'
###[ DHCP options ]### 
              options   = [message-type='discover' end]

................Begin emission:
Finished to send 1 packets.
....................................*.................................................................................^C
Received 134 packets, got 1 answers, remaining 0 packets
DHCP Server - MAC: a4:xx:xx:xx:xx:xx, IP: 192.168.10.1

 

ラズパイへ接続できない環境での注意点

ラズパイへ接続できない環境へ移動して、リモートデバッグ設定をしたプロジェクトを開くと、

Couldn't refresh skeletons for remote interpreter: Error connecting to remote host 192.168.10.50

というメッセージが表示されました。

また、 Automatic update を有効にしていると、ラズパイへの接続を常に探しているようでした。その結果、ファンが回りっぱなしになるなど、負荷の高まりを感じました。

 
そのため、ラズパイへ接続できない環境へ移動した場合は、

  • Python SDKを、Remoteからローカルのものに切り替える
  • Automatic Updateを解除する

などを行ったほうが良さそうです。

最新のScapy(2.3.3.dev957)では、Macにpcapやdnetモジュールのインストールが不要っぽい

前回の記事の中で、MacへのScapyのインストールについて触れました。
Mac + Python2 + Scapyで、DHCPサーバを探してみた - メモ的な思考的な

 
公開後、Twitterにて

とのツイートをいただきました。

そこで、pcapやdnetモジュールが不要かどうかを試してみました。

 
目次

 

環境

 

NG: PyPIにあるScapy (v2.3.3)を使う場合

PyPIからpip installした場合です。

# Mac OS Xのバージョンを確認
$ sw_vers -productVersion
10.11.6

# Pythonのバージョン確認
$ python --version
Python 2.7.14

# virtualenv環境を有効化
$ virtualenv envdev
...
Installing setuptools, pip, wheel...done.

$ source envdev/bin/activate

# pipの状態を確認
(envdev) $ pip list
pip (9.0.1)
setuptools (38.4.0)
wheel (0.30.0)

# scapyをインストール
(envdev) $ pip install scapy
Collecting scapy
Installing collected packages: scapy
Successfully installed scapy-2.3.3

# インストール後の確認
(envdev) $ pip list
pip (9.0.1)
scapy (2.3.3)
setuptools (38.4.0)
wheel (0.30.0)

 
試しにコードを実行してみると、エラーになりました。

(envdev) $ python discover_dhcp_server.py 
Traceback (most recent call last):
  File "discover_dhcp_server.py", line 3, in <module>
    from scapy.layers.dhcp import DHCP, BOOTP
  File "/path/to/envdev/lib/python2.7/site-packages/scapy/layers/dhcp.py", line 14, in <module>
    from scapy.ansmachine import *
  File "/path/to/envdev/lib/python2.7/site-packages/scapy/ansmachine.py", line 14, in <module>
    from scapy.sendrecv import send,sendp,sniff
  File "/path/to/envdev/lib/python2.7/site-packages/scapy/sendrecv.py", line 14, in <module>
    from scapy.arch.consts import DARWIN, FREEBSD
  File "/path/to/envdev/lib/python2.7/site-packages/scapy/arch/__init__.py", line 79, in <module>
    from scapy.arch.pcapdnet import *
  File "/path/to/envdev/lib/python2.7/site-packages/scapy/arch/pcapdnet.py", line 328, in <module>
    import pcapy as pcap
ImportError: No module named pcapy

 

NG: GitHubのタグ「v2.3.3」を使う場合

次にGitHubにあるソースコードからインストールしてみます。

まずは、タグ v2.3.3 を指定してインストールしてみます。

なお、タグやコミットなどを指定してpip installする方法は、以下に記載がありました。
Install specific git commit with pip - Stack Overflow

# virtualenvを抜ける
$ deactivate

# 別のvirtualenvを構築して、そちらにインストール
$ virtualenv envdev2
$ source envdev2/bin/activate

(envdev2) $ pip list
pip (9.0.1)
setuptools (38.4.0)
wheel (0.30.0)

# v2.3.3を指定してインストール
(envdev2) $ pip install git+https://github.com/secdev/scapy.git@v2.3.3
Collecting git+https://github.com/secdev/scapy.git@v2.3.3
  Cloning https://github.com/secdev/scapy.git (to v2.3.3) to /private/var/folders/h0/l5plp4zd3517r988jpm481g00000gn/T/pip-as1YTj-build
Installing collected packages: scapy
  Running setup.py install for scapy ... done
Successfully installed scapy-2.3.3

# インストールを確認
(envdev2) $ pip list
pip (9.0.1)
scapy (2.3.3)
setuptools (38.4.0)
wheel (0.30.0)

 
試しにコードを実行してみると、エラーになりました。

(envdev2) $ python discover_dhcp_server.py 
Traceback (most recent call last):
  File "dhcp/discover_dhcp_server.py", line 3, in <module>
    from scapy.layers.dhcp import DHCP, BOOTP
  File "/path/to/envdev2/lib/python2.7/site-packages/scapy/layers/dhcp.py", line 14, in <module>
    from scapy.ansmachine import *
  File "/path/to/envdev2/lib/python2.7/site-packages/scapy/ansmachine.py", line 14, in <module>
    from scapy.sendrecv import send,sendp,sniff
  File "/path/to/envdev2/lib/python2.7/site-packages/scapy/sendrecv.py", line 14, in <module>
    from scapy.arch.consts import DARWIN, FREEBSD
  File "/path/to/envdev2/lib/python2.7/site-packages/scapy/arch/__init__.py", line 79, in <module>
    from scapy.arch.pcapdnet import *
  File "/path/to/envdev2/lib/python2.7/site-packages/scapy/arch/pcapdnet.py", line 328, in <module>
    import pcapy as pcap
ImportError: No module named pcapy

 

OK: GitHubの最新コミットを使う場合

最後に、現時点の最新コミット aa11c749d563c6b4a8cfa88182ae8cfaae2edbf7ソースコードをインストールしてみます。

# 再度、別のvirtualenv環境を作る
(envdev2) $ deactivate
$ virtualenv envdev3
$ source envdev3/bin/activate

(envdev3) $ pip list
pip (9.0.1)
setuptools (38.4.0)
wheel (0.30.0)

# インストール
(envdev3) $ pip install git+https://github.com/secdev/scapy.git@aa11c749d563c6b4a8cfa88182ae8cfaae2edbf7
Collecting git+https://github.com/secdev/scapy.git@aa11c749d563c6b4a8cfa88182ae8cfaae2edbf7
  Cloning https://github.com/secdev/scapy.git (to aa11c749d563c6b4a8cfa88182ae8cfaae2edbf7) to /private/var/folders/h0/l5plp4zd3517r988jpm481g00000gn/T/pip-3zebOf-build
  Could not find a tag or branch 'aa11c749d563c6b4a8cfa88182ae8cfaae2edbf7', assuming commit.
Installing collected packages: scapy
  Running setup.py install for scapy ... done
Successfully installed scapy-2.3.3.dev957

# インストール後の確認
(envdev3) $ pip list
pip (9.0.1)
scapy (2.3.3.dev957)
setuptools (38.4.0)
wheel (0.30.0)

 
試しにコードを実行すると、問題なく動作しました。

(envdev3) $ python discover_dhcp_server.py 
###[ Ethernet ]### 
  dst       = FF:FF:FF:FF:FF:FF
  src       = 00:00:00:00:00:04
  type      = 0x800
###[ IP ]### 
     version   = 4
     ihl       = None
     tos       = 0x0
     len       = None
     id        = 1
     flags     = 
     frag      = 0
     ttl       = 64
     proto     = udp
     chksum    = None
     src       = 0.0.0.0
     dst       = 255.255.255.255
     \options   \
###[ UDP ]### 
        sport     = bootpc
        dport     = bootps
        len       = None
        chksum    = None
###[ BOOTP ]### 
           op        = BOOTREQUEST
           htype     = 1
           hlen      = 6
           hops      = 0
           xid       = 0
           secs      = 0
           flags     = 
           ciaddr    = 0.0.0.0
           yiaddr    = 0.0.0.0
           siaddr    = 0.0.0.0
           giaddr    = 0.0.0.0
           chaddr    = '\x00\x00\x00\x00\x00\x04'
           sname     = ''
           file      = ''
           options   = 'c\x82Sc'
###[ DHCP options ]### 
              options   = [message-type='discover' end]

Begin emission:
Finished to send 1 packets.
..........^C
Received 10 packets, got 0 answers, remaining 1 packets

 
これより、GitHubの最新コミットから持ってくれば

  • pcap
  • dnet

モジュールは不要なようです。

 
guedouさん、教えていただきありがとうございました。

Mac + Python2 + Scapyで、DHCPサーバを探してみた

Scapyを使ってDHCPサーバを探してみようと、実装方法を調べてみました。

すると、Scapyの公式ドキュメントにあったり、既に実装している方々がいました。

 
ただ、自分でも手を動かして理解したかったので、実装してみたときのメモを残します。

なお、DHCPサーバを探すツールは、英語版Wikipediaにまとまっていました。
Rogue DHCP - Wikipedia

 
目次

 

環境

ネットワーク構成
+--------+
| switch |
+-+-+----+
| | |        +----------------+
| | +--------+ Mac OS X       |
| |          | 192.168.10.101 |
| |          +----------------+
| |
| |          +----------------+
| +----------+ DHCP1          |
|            | 192.168.10.1   |
|            +----------------+
|
|            +----------------+
+------------+ DHCP2          |
             | 192.168.10.110 |
             +----------------+

 

各機器の構成

 

インストール

Scapyはvirtualenv環境にインストールします。

# virtualenv環境を作る
$ virtualenv env
...
Installing setuptools, pip, wheel...done.

# virtualenv環境を有効化する
$ source env/bin/activate

# Scapyをインストールする
(env) $ pip install scapy
...
Successfully installed scapy-2.3.3

 
他にも必要なライブラリがあるためインストールしてみましたが、当初うまくいきませんでした。

そのため、NG/OKなインストール例をメモしておきます。

なお、discover_dhcp_server.py というDHCPサーバを見つけるPythonスクリプトは正常に動作するものとします。

 

Scapyで送信できないインストール例

Scapyだけをインストールして実行してみます。

# インストール状況の確認
(env) $ pip list
pip (9.0.1)
scapy (2.3.3)
setuptools (38.4.0)
wheel (0.30.0)

# 実行
(env) $ python discover_dhcp_server.py 
Traceback (most recent call last):
  File "discover_dhcp_server.py", line 3, in <module>
    from scapy.layers.dhcp import DHCP, BOOTP
  File "/path/to/env/lib/python2.7/site-packages/scapy/layers/dhcp.py", line 14, in <module>
    from scapy.ansmachine import *
  File "/path/to/env/lib/python2.7/site-packages/scapy/ansmachine.py", line 14, in <module>
    from scapy.sendrecv import send,sendp,sniff
  File "/path/to/env/lib/python2.7/site-packages/scapy/sendrecv.py", line 14, in <module>
    from scapy.arch.consts import DARWIN, FREEBSD
  File "/path/to/env/lib/python2.7/site-packages/scapy/arch/__init__.py", line 79, in <module>
    from scapy.arch.pcapdnet import *
  File "/path/to/env/lib/python2.7/site-packages/scapy/arch/pcapdnet.py", line 328, in <module>
    import pcapy as pcap
ImportError: No module named pcapy

 
モジュール pcapy が無いようなので、インストールします。

(env) $ pip install pcapy
...
Successfully installed pcapy-0.11.1

 
再度実行します。

(env) $ python discover_dhcp_server.py 
Traceback (most recent call last):
  File "dhcp/discover_dhcp_server.py", line 3, in <module>
    from scapy.layers.dhcp import DHCP, BOOTP
  File "/path/to/env/lib/python2.7/site-packages/scapy/layers/dhcp.py", line 14, in <module>
    from scapy.ansmachine import *
  File "/path/to/env/lib/python2.7/site-packages/scapy/ansmachine.py", line 14, in <module>
    from scapy.sendrecv import send,sendp,sniff
  File "/path/to/env/lib/python2.7/site-packages/scapy/sendrecv.py", line 14, in <module>
    from scapy.arch.consts import DARWIN, FREEBSD
  File "/path/to/env/lib/python2.7/site-packages/scapy/arch/__init__.py", line 79, in <module>
    from scapy.arch.pcapdnet import *
  File "/path/to/env/lib/python2.7/site-packages/scapy/arch/pcapdnet.py", line 471, in <module>
    import dumbnet as dnet
ImportError: No module named dumbnet

別のモジュール dumbnet が無いようです。

 
PyPIで探しますが、それらしいライブラリはありませんでした。
https://pypi.python.org/pypi?%3Aaction=search&term=dumbnet&submit=search

調べてみると、stackoverflowに

dumbnet is also known as libdnet.

python - 'ImportError: No module named dumbnet' when trying to run a script that leverages scapy on OS X - Stack Overflow

とあり、ソースコードからインストールすれば良いと書かれていました。

そのため、ソースコードからインストールしてみます。

# インストール用のディレクトリを作成し、git clone
(env) $ mkdir install_libdnet
(env) $ cd install_libdnet/
(env) install_libdnet $ git clone https://github.com/dugsong/libdnet.git
...
Resolving deltas: 100% (2709/2709), done.

# インストール
(env) install_libdnet $ cd libdnet
(env) libdnet $ ./configure && make
...
(env) libdnet $ cd python
(env) python $ python setup.py install
...
Writing /path/to/env/lib/python2.7/site-packages/dnet-1.12-py2.7.egg-info

 
もう一度実行してみます。

# ディレクトリを戻す
(env) $ cd ../../../

# 実行
(env) $ python discover_dhcp_server.py 
Begin emission:
Finished to send 1 packets.
.**Traceback (most recent call last):
File "discover_dhcp_server.py", line 66, in <module>
  discover()
File "discover_dhcp_server.py", line 58, in discover
  answers, _ = srp(discover_packet, iface=USB_INTERFACE_NAME, multi=True)
File "/path/to/env/lib/python2.7/site-packages/scapy/sendrecv.py", line 378, in srp
  a,b=sndrcv(s ,x,*args,**kargs)
File "/path/to/env/lib/python2.7/site-packages/scapy/sendrecv.py", line 133, in sndrcv
  r = pks.nonblock_recv()
File "/path/to/env/lib/python2.7/site-packages/scapy/arch/pcapdnet.py", line 672, in nonblock_recv
  p = self.recv(MTU)
File "/path/to/env/lib/python2.7/site-packages/scapy/arch/pcapdnet.py", line 653, in recv
  pkt = self.ins.next()
File "/path/to/env/lib/python2.7/site-packages/scapy/arch/pcapdnet.py", line 390, in next
  s,us = h.getts()
AttributeError: 'NoneType' object has no attribute 'getts'

また別のエラーが出ました。今度はモジュール以外のエラーです。

 
事例を調べたところ、ScapyのIssueに情報がありました。

In a bash shell, i ran: sudo pip install pypcap Instead of sudo pip install pcap.

Sniff function Is not Functioning on OS X · Issue #97 · secdev/scapy

pypcapをインストールして試してみます。

# pypcapのインストール
(env) $ pip install pypcap
...
Successfully installed pypcap-1.2.0

# 実行
(env) $ python discover_dhcp_server.py 
Begin emission:
Finished to send 1 packets.
.**.....

送受信が成功しました。

 

GitHubの最新コミットを利用する場合 (2017/1/9追記)

GitHubの最新コミットを使えば、

  • pypcap
  • libdnet

がなくても動作するようです。

試してみた内容を以下の記事にしました。
最新のScapy(2.3.3.dev957)では、Macにpcapやdnetモジュールのインストールが不要っぽい - メモ的な思考的な

 

Scapyで送信できるインストール例

上記より、

  • pipでインストール
    • Scapy
    • pypcap
  • ソースコードからインストール
    • libdnet

のセットアップで良さそうです。

# Pythonバージョンを確認
$ python --version
Python 2.7.14

# virtualenv環境を作って有効化
$ virtualenv env
...
Installing setuptools, pip, wheel...done.
$ source env/bin/activate

# pipでScapyとpypcapをインストール
(env) $ pip install scapy pypcap
...
Successfully installed pypcap-1.2.0 scapy-2.3.3

# ソースコードからlibdnetをインストール
(env) $ mkdir install_libdnet
(env) $ cd install_libdnet/
(env) $ git clone https://github.com/dugsong/libdnet.git
(env) $ ./configure && make
(env) $ cd python/
(env) $ python setup.py install
...
Writing /path/to/env/lib/python2.7/site-packages/dnet-1.12-py2.7.egg-info

# インストール後の状況を確認
(env) $ pip list
dnet (1.12)
pip (9.0.1)
pypcap (1.2.0)
scapy (2.3.3)
setuptools (38.4.0)
wheel (0.30.0)

# 動作確認
(env) $ cd ../../../
(env) $ python discover_dhcp_server.py 
Begin emission:
Finished to send 1 packets.
.**....

動作しました。

 

野良DHCPサーバを探す実装

処理の流れ

公式ドキュメントや他の実装のコードを読みつつ、実装していきます。

主な処理の流れとしては、

  • Ether, IP, UDP, BOOTP, DHCP の各レイヤを作る
  • 公式ドキュメントに従い、送信前に conf.checkIPaddr = False とセット
  • L2パケットとして、srp(discover_packet, iface=USB_INTERFACE_NAME, multi=True) として送信
    • multi=True は公式ドキュメント通り

です。

 

BOOTPオブジェクトについて

BOOTPレイヤを作るため、 scapy.layers.dhcp.BOOTP クラスのインスタンスを作成します。

コンストラクタ引数の chaddr にはクライアントのMACアドレスを渡します。今回は野良DHCPサーバを探すため、実際のインタフェースのMACアドレスを渡すのではなく、適当な値を渡すことにします。

 
また、引数のMACアドレスの形式については、

  • MACアドレスをそのまま渡す
  • 何かしらの変換をして渡す

の2つの記事がありました。ソースコードのdocstringには何も記載されていませんでした。

そこで、RFC2131を見てみましたが、

chaddr 16 クライアントのハードウェアアドレス。

http://srgia.com/docs/rfc2131j.html#tbl1 (原文) https://www.ietf.org/rfc/rfc2131.txt

としか書かれておらず、形式については触れられていませんでした。

 
試しに両方で実装してみると、

# MACアドレスをそのまま渡すように実装した場合の結果
Begin emission:
Finished to send 1 packets.
.*****.........................^C
Received 31 packets, got 5 answers, remaining 0 packets
DHCP Server - MAC: bc:xx:xx:xx:xx:xx, IP: 192.168.10.110
DHCP Server - MAC: a4:xx:xx:xx:xx:xx, IP: 192.168.10.1
DHCP Server - MAC: a4:xx:xx:xx:xx:xx, IP: 192.168.10.1
DHCP Server - MAC: a4:xx:xx:xx:xx:xx, IP: 192.168.10.1
DHCP Server - MAC: a4:xx:xx:xx:xx:xx, IP: 192.168.10.1

# MACアドレスを変換した場合の結果
Begin emission:
Finished to send 1 packets.
.**.................................^C
Received 36 packets, got 2 answers, remaining 0 packets
DHCP Server - MAC: bc:xx:xx:xx:xx:xx, IP: 192.168.10.110
DHCP Server - MAC: a4:xx:xx:xx:xx:xx, IP: 192.168.10.1

と、MACアドレスそのものを渡した場合にはうまく動作していないように見えました。

 
では、どんなロジックで変換すればよいか、参考にしたソースコードを見てみると

# http://netbuffalo.doorblog.jp/archives/4278096.html
chaddr = ''.join([chr(int(x,16)) for x in mac.split(':')])

# https://github.com/david415/dhcptakeover/blob/master/dhcptakeover.py
fam,hw = scapy.all.get_if_raw_hwaddr(scapy.all.conf.iface)
scapy.all.BOOTP(chaddr=hw)

となっていました。

get_if_raw_hwaddr()の実装をIntelliJ IDEAで追ってみると、

# https://github.com/secdev/scapy/blob/v2.3.3/scapy/arch/pcapdnet.py#L509
mac = mac2str(str(link_addr))

# https://github.com/secdev/scapy/blob/v2.3.3/scapy/utils.py#L294
def mac2str(mac):
    return "".join(map(lambda x: chr(int(x,16)), mac.split(":")))

となっていました。

参考にしたソースコード間でロジックが一致したため、 scapy.utils.mac2str() を使えば良さそうでした。

 

ソースコード全体

discover_dhcp_server.py

# -*- coding: utf-8 -*-
from scapy.config import conf
from scapy.layers.dhcp import DHCP, BOOTP
from scapy.layers.inet import IP, UDP
from scapy.layers.l2 import Ether
from scapy.sendrecv import srp
from scapy.utils import mac2str

DHCP_CLIENT_MAC_ADDRESS = '00:00:00:00:00:04'
BROADCAST_MAC_ADDRESS = 'FF:FF:FF:FF:FF:FF'

LIMITED_BROADCAST_IP_ADDRESS = '255.255.255.255'
DHCP_DISCOVER_HOST_IP_ADDRESS = '0.0.0.0'

DHCP_SERVER_PORT = 67
DHCP_CLIENT_PORT = 68

USB_INTERFACE_NAME = 'en4'


def discover():
    ether_layer = Ether(
        src=DHCP_CLIENT_MAC_ADDRESS,
        dst=BROADCAST_MAC_ADDRESS,
    )

    ip_layer = IP(
        src=DHCP_DISCOVER_HOST_IP_ADDRESS,
        dst=LIMITED_BROADCAST_IP_ADDRESS,
    )

    udp_layer = UDP(
        sport=DHCP_CLIENT_PORT,
        dport=DHCP_SERVER_PORT,
    )

    chaddr = mac2str(DHCP_CLIENT_MAC_ADDRESS)
    bootp_layer = BOOTP(chaddr=chaddr)

    dhcp_layer = DHCP(options=[('message-type', 'discover'), 'end'])

    discover_packet = ether_layer / ip_layer / udp_layer / bootp_layer / dhcp_layer

    # 作成したパケットを念のため確認
    discover_packet.show()

    # Scapyのドキュメント通りに設定して送信
    # http://scapy.readthedocs.io/en/latest/usage.html#identifying-rogue-dhcp-servers-on-your-lan
    conf.checkIPaddr = False
    answers, _ = srp(discover_packet, iface=USB_INTERFACE_NAME, multi=True)

    for send_packet, recv_packet in answers:
        print 'DHCP Server - MAC: {}, IP: {}'.format(
            recv_packet[Ether].src, recv_packet[IP].src)


if __name__ == '__main__':
    discover()

 

実行

Macで実行する時の注意点

手元の環境が

  • OS X 10.11.6
  • pyenvのPython2を使う
  • virtualenvに入れたScapyを使う

という環境のせいかもしれませんが、単に実行しただけでは

(env) $ python discover_dhcp_server.py
Begin emission:
Finished to send 1 packets.
..................(略)...^C
Received 272 packets, got 0 answers, remaining 1 packets

と、パケットは送信されるものの受信ができませんでした。

そのため、

  • WiFiではなく、外付けUSB有線LANアダプタ(LUA4-U3-AGT)からパケットを送信
  • Wiresharkを起動し、外付けUSB有線LANアダプタ(LUA4-U3-AGT)をモニタ

として実行すると、

Begin emission:
Finished to send 1 packets.
.*....*........................^C
Received 31 packets, got 2 answers, remaining 0 packets

受信もできました。 * の部分が、送受信が成功したところです。

 
ちなみに、実行時のログ全体は以下の通りです。

なお、 Ctrl + C で停止しない限り、パケットを送信し続けます。

$ python dhcp/discover_dhcp_server.py 
###[ Ethernet ]### 
  dst       = FF:FF:FF:FF:FF:FF
  src       = 00:00:00:00:00:04
  type      = 0x800
###[ IP ]### 
     version   = 4
     ihl       = None
     tos       = 0x0
     len       = None
     id        = 1
     flags     = 
     frag      = 0
     ttl       = 64
     proto     = udp
     chksum    = None
     src       = 0.0.0.0
     dst       = 255.255.255.255
     \options   \
###[ UDP ]### 
        sport     = bootpc
        dport     = bootps
        len       = None
        chksum    = None
###[ BOOTP ]### 
           op        = BOOTREQUEST
           htype     = 1
           hlen      = 6
           hops      = 0
           xid       = 0
           secs      = 0
           flags     = 
           ciaddr    = 0.0.0.0
           yiaddr    = 0.0.0.0
           siaddr    = 0.0.0.0
           giaddr    = 0.0.0.0
           chaddr    = '\x00\x00\x00\x00\x00\x04'
           sname     = ''
           file      = ''
           options   = 'c\x82Sc'
###[ DHCP options ]### 
              options   = [message-type='discover' end]

Begin emission:
Finished to send 1 packets.
.*....*........................^C
Received 31 packets, got 2 answers, remaining 0 packets
DHCP Server - MAC: bc:xx:xx:xx:xx:xx, IP: 192.168.10.110
DHCP Server - MAC: a4:xx:xx:xx:xx:xx, IP: 192.168.10.1

 

ソースコード

GitHubに上げました。 dhcp/discover_dhcp_server.py が今回のファイルです。
https://github.com/thinkAmi-sandbox/scapy-sample

Python2 + Scapyで、Raspberry Pi 2 Model B をNAT/NAPTルータ化してみた #router_jisaku

前回・前々回で、Python2 + Scapyで、Raspberry Pi 2 Model B をブリッジ化してみました。

 
ルーター自作でわかるパケットの流れ」に従い、次はルータを作ります。

ルーター自作でわかるパケットの流れ

ルーター自作でわかるパケットの流れ

 
ただ、書籍ではL2ルータを実装していましたが、NAT/NAPTなルータが欲しかったので、L3ルータを作成してみました。

 
目次

 

環境

NAT/NAPTによるIPアドレス変換があるため、今回は

  • 192.168.10.0/24
  • 192.168.11.0/24

の環境を用意します。

各機器の役割は、ブリッジの時と同じように

  • Mac = HTTPサーバ
  • 無線LANアクセスポイント = スイッチ
  • ラズパイ = NAT/NAPTルータ
  • Windows10 = ルータにぶら下げたクライアント

とします。

 

ネットワーク構成

各機器の接続は、ブリッジの時と同じです。

-----------------------------------------
Mac
(`en0` : WiFi)
-----------------------------------------
|
|
-----------------------------------------
(無線)
無線LANアクセスポイント
(スイッチ)
-----------------------------------------
|
|
-----------------------------------------
(`eth0` : オンボードLANアダプタ)
Raspberry Pi 2 Model B
(`eth1` : 外付けUSB有線LANアダプタ)
-----------------------------------------
|
|
-----------------------------------------
(オンボードLANアダプタ)
Windows10
-----------------------------------------

 

ネットワークアダプタ設定

明示的に設定したネットワークアダプタ設定です。

なお、以降の表記は

とします。

機器 アダプタ IPアドレス DGW DNS
Mac en0 192.168.10.101/24 192.168.10.1 192.168.10.1
無線LAN AP 192.168.10.1/24
ラズパイ eth0 192.168.10.50/24 192.168.10.1 192.168.10.1
ラズパイ eth1 192.168.11.60/24
Windows 192.168.11.201/24 192.168.11.60 192.168.10.1

 
ラズパイ上の状態もこんな感じです。

# インタフェースのIPアドレス
pi@raspberrypi:~ $ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.10.50  netmask 255.255.255.0  broadcast 192.168.10.255
eth1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.11.60  netmask 255.255.255.0  broadcast 192.168.11.255

# ルーティング情報
pi@raspberrypi:~ $ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.10.1    0.0.0.0         UG    202    0        0 eth0
192.168.10.0    0.0.0.0         255.255.255.0   U     202    0        0 eth0
192.168.11.0    0.0.0.0         255.255.255.0   U     203    0        0 eth1

 

各機器の構成

こちらもブリッジの時と同じです。

  • Mac OS X 10.11.6
    • 疎通確認のために、PythonでHTTPサーバを立てる
  • Raspberry Pi 2 Model B
    • Raspbian
      • RASPBIAN STRETCH WITH DESKTOP
      • Version: November 2017
    • Python 2.7.13
      • Raspbianにインストールされていたものを使用
    • Scapy 2.3.3
    • 外付けUSB有線LANアダプタ(LUA4-U3-AGT)
    • SSHMacから接続可能
  • Windows10
    • curlをインストール済

 

Scapyの関数を調べる

ルータ化するにあたり、Scapyで使えそうな関数を調べてみます。

 

インタフェースのMACアドレスIPアドレス取得

Scapyを実行する機器のインタフェースからMACアドレスIPアドレスを取得するには、

を使います。

関数の引数には、インタフェース名(下記の例だと eth0 )を渡します。

from scapy.all import get_if_hwaddr, get_if_addr

global_eth_mac_address = get_if_hwaddr('eth0')
global_eth_ip_address = get_if_addr('eth0')

 

パケット中のレイヤの存在確認とレイヤ取得

パケットの各レイヤの存在確認と取得は、パケットの

  • レイヤの存在確認: haslayer()
  • レイヤ取得: getlayer()

を使います。

from scapy.all import conf
from scapy.layers.inet import TCP

s = conf.L2socket(iface='eth0')
p = s.recv()

# IPレイヤ存在確認
has_tcp = p.haslayer(IP)

# IPレイヤ取得
tcp_layer = p.getlayer(IP)

 
なお、getlayer()では、該当レイヤよりも上のレイヤを取得します。

例えば p.getlayer(IP) の場合、IPとその上のTCPを取得します。

# -*- coding: utf-8 -*-
from select import select

from scapy.all import conf
from scapy.layers.inet import IP

while True:
    l2s = conf.L2socket(iface='eth0')
    r, _, _ = select([l2s], [], [])
    for s in r:
        p = s.recv()
        if p and p.haslayer(IP):
            layer = p.getlayer(IP)
            layer.show()

# => show()の結果 
#     ###[ IP ]### 
# version   = 4L
# ihl       = 5L
# tos       = 0x0
# len       = 64
# id        = 37143
# flags     = DF
# frag      = 0L
# ttl       = 64
# proto     = tcp
# chksum    = 0x13b9
# src       = 192.168.10.101
# dst       = 192.168.10.50
# \options \
#     ###[ TCP ]### 
# sport     = 56898
# dport     = http
# seq       = 1880557107
# ack       = 0
# dataofs   = 11L
# reserved  = 0L
# flags     = S
# window    = 65535
# chksum    = 0xa411
# urgptr    = 0
# options   = [('MSS', 1460), ('NOP', None), ('WScale', 5), ('NOP', None), ('NOP', None), ('Timestamp', (591430886, 0)), ('SAckOK', ''), ('EOL', None)]

 

レイヤ属性へのアクセス

上のshow()の結果を見ると、IPレイヤには chksum 属性があります。

アクセスするには、ドット(.)と属性名を使います。

# IPレイヤのチェックサム(chksum) へアクセス
ip_packet.chksum

# TCPレイヤの宛先ポートへのアクセス
tcp_layer = target_packet.getlayer(TCP)
dport = tcp_layer.dport

 

パケットの送信

送信方法は色々とありますが、今回は send() 関数を使います。

send関数は、レイヤ3以上(IPレイヤ)以上のパケットを送信します。その際のレイヤ2はScapyがいい感じに作成します。

send(packet_to_global, verbose=0)

 
なお、引数 verbose は、送信ログをコンソール出力するかを決めます。

デフォルト値は 2 のようで

.
Sent 1 packets.

が出力されます。

verbose=0 の場合、コンソールへ出力しません。

 

チェックサムの再計算

IP・TCPUDPにはチェックサムがあります。

そのため、内容を変更した場合にはチェックサムを再計算する必要があります。

Scapyでの再計算を調べてみると、del文でチェックサムを削除した後でパケットを再生成すれば良いようです。
python - How to calculate a packet checksum without sending it? - Stack Overflow

del ip_packet.chksum
result = IP(str(ip_packet))

 

パケットの情報表示
  • show()
  • summary()
  • mysummary()

があります。

それぞれの違いは以下です。

layer = p.getlayer(IP)
print '---- show() ----'
layer.show()
print '---- summary() ----'
print layer.summary()
print '---- mysummary() ----'
print layer.mysummary()

# => 実行結果
# ---- show() ----
# ###[ IP ]### 
# version   = 4L
# ihl       = 5L
# tos       = 0x0
# len       = 64
# id        = 19636
# flags     = DF
# frag      = 0L
# ttl       = 64
# proto     = tcp
# chksum    = 0x581c
# src       = 192.168.10.101
# dst       = 192.168.10.50
# \options \
#     ###[ TCP ]### 
# sport     = 56968
# dport     = http
# seq       = 2173034460L
# ack       = 0
# dataofs   = 11L
# reserved  = 0L
# flags     = S
# window    = 65535
# chksum    = 0x8485
# urgptr    = 0
# options   = [('MSS', 1460), ('NOP', None), ('WScale', 5), ('NOP', None), ('NOP', None), ('Timestamp', (592296199, 0)), ('SAckOK', ''), ('EOL', None)]
# 
# ---- summary() ----
# IP / TCP 192.168.10.101:56968 > 192.168.10.50:http S
# ---- mysummary() ----
# 192.168.10.101 > 192.168.10.50 tcp

 

RSTフラグの立ったパケットをルータから送信しない方法の調査

今回は厳密なルータを作成するわけではないため、NAT/NAPTした時に発生するRST(Reset)フラグの立ったパケットについては、ルータから送信しないようにします。

これを送信してしまうと、TCPのやり取りが途中で中断されてしまうため、NAT/NAPTがうまく動かないです。
NAT の理解は芋づる式 - インフラまわりのプロになりたい

 
そのため、

os.system('sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP')

と、iptablesのルールを追加します。

なお、iptablesのオプションの意味は、

  • -A OUTPUT
    • OUTPUTチェイン(ローカルで生成されたパケットをルーティングの前に変換するためのチェイン)に、規則を追加する
  • -p tcp
  • --tcp-flags RST RST
    • RSTフラグがセットされているTCPパケットに対する処理
  • -j DROP
    • ここまでの規則にマッチしたパケットを破棄する

です。

参考:

 

NATルータの実装

ここまでで調査が終わったため、次はルータを実装していきます。

なお、前回、I/O処理をいろいろと試しましたが、今回はScapyのsniff()関数に合わせて、I/O多重化の select() だけの実装例となります。

また、MacからSSHでラズパイに接続していることもあり、SSHのパケットが入ってきます。動作が遅くなったりするので、SSHパケットの場合は処理しないようにしています。本来は不要な処理ですが...

scapy_nat_router_using_ip_layer.py

# -*- coding: utf-8 -*-
from scapy.all import conf, send, get_if_hwaddr, get_if_addr
from select import select
from scapy.layers.inet import IP, TCP, UDP
import os


def recreate_ip_packet(ip_packet):
    # 各レイヤのチェックサムは、削除することでScapyが再計算してくれる
    # https://stackoverflow.com/questions/5953371/how-to-calculate-a-packet-checksum-without-sending-it
    if ip_packet.haslayer(TCP):
        del ip_packet.getlayer(TCP).chksum
    if ip_packet.haslayer(UDP):
        del ip_packet.getlayer(UDP).chksum
    del ip_packet.chksum

    # 再計算するために、IPレイヤより上層のパケットインスタンスを生成する
    result = IP(str(ip_packet))
    return result


def is_ssh_packet(target_packet):
    if not target_packet.haslayer(TCP):
        return False
    tcp_layer = target_packet.getlayer(TCP)
    return tcp_layer.dport == 22


def nat_router():
    try:
        global_socket = conf.L2socket(iface='eth0')
        local_socket = conf.L2socket(iface='eth1')

        # ルータのインタフェースのMACアドレス・IPアドレスを取得しておく
        global_eth_mac_address = get_if_hwaddr('eth0')
        global_eth_ip_address = get_if_addr('eth0')
        local_eth_mac_address = get_if_hwaddr('eth1')

        # RSTパケットをラズパイから送信すると、TCPが途中で切れてしまうので、送信しないようにする
        os.system('sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP')

        # NATテーブル
        nat_table = {}

        while True:
            readable_sockets, _, _ = select([global_socket, local_socket], [], [])

            for s in readable_sockets:
                p = s.recv()

                if not p or not p.haslayer(IP):
                    continue

                # 今回の場合、MacとラズパイをSSHでつないでいることからSSHパケットは除外する
                # 除外しないとパケット量が増えてしまい、curlがタイムアウトしてしまう
                if is_ssh_packet(p):
                    continue

                # ローカルからグローバルへ抜けるパケットが入ってきた場合
                if p.dst == local_eth_mac_address:
                    # IPレイヤのパケットを取得する
                    packet_from_local_to_global = p.getlayer(IP)
                    # レスポンスパケットのIPアドレスを変換するため、
                    # 変換後のIPアドレスをキーとして、変換前のIPアドレスを保管しておく
                    nat_table[global_eth_ip_address] = packet_from_local_to_global.src
                    # 送信元IPアドレスを、ルーターのグローバル側IPアドレスに差し替える
                    packet_from_local_to_global.src = global_eth_ip_address
                    # 送信用パケットを再生成する
                    packet_to_global = recreate_ip_packet(packet_from_local_to_global)
                    # 送信用パケットの概要をコンソールに表示する
                    print p.summary()

                    # send()はLayer3のパケットをいい感じに送信してくれる
                    # verbose=2(デフォルト値)など、1~3の場合、以下が表示される
                    # .
                    # Sent 1 packets.
                    # verbose=0の場合、何も出力されない
                    send(packet_to_global, verbose=0)

                # グローバルからローカルへ抜けるパケットが入ってきた場合
                elif p.dst == global_eth_mac_address:
                    packet_from_global_to_local = p.getlayer(IP)
                    # 変換後のIPアドレスから変換前IPアドレスを取得する
                    local_pc_ip_address = nat_table.get(packet_from_global_to_local.dst)
                    # 念のため、変換できない場合はパケットを送信しない
                    if local_pc_ip_address is None:
                        continue
                    # 宛先IPアドレスを変換前IPアドレスに差し替える
                    packet_from_global_to_local.dst = local_pc_ip_address
                    
                    packet_to_local = recreate_ip_packet(packet_from_global_to_local)
                    print p.summary()
                    send(packet_to_local, verbose=0)

    except KeyboardInterrupt:
        pass


if __name__ == '__main__':
    nat_router()

 
NAT処理は

# ローカルからグローバルへの送信
## NATテーブルへローカルIPアドレスを保持
nat_table[global_eth_ip_address] = packet_from_local_to_global.src
## 送信元IPアドレスを、ルーターのグローバル側IPアドレスに差し替える
packet_from_local_to_global.src = global_eth_ip_address

# グローバルからローカルへの送信
local_pc_ip_address = nat_table.get(packet_from_global_to_local.dst)
## 念のため、変換できない場合はパケットを送信しない
if local_pc_ip_address is None:
    continue
## 宛先IPアドレスを変換前IPアドレスに差し替える
packet_from_global_to_local.dst = local_pc_ip_address

という部分になります。このあと、IPパケットを再生成しています。

今回の通信の始まりはローカル側からの想定なため、上記のような簡易的なものにしてあります。

 

NATの動作確認

ラズパイをNATルータ化

作成したPythonスクリプトを実行し、ルータ化します。

pi@raspberrypi:~/router_jisaku $ sudo python scapy_nat_router_using_ip_layer.py 

 

tcpdumpでのパケット確認

パケットのやり取りを確認するため、ラズパイの別のターミナルで tcpdump を実行しておきます。

pi@raspberrypi:~ $ sudo tcpdump -i eth0 -e port 8000 -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes

なお、tcpdumpのオプションの意味は、

  • -i eth0
    • インタフェースeth0に対し、パケット確認する
  • -e
    • リンクレベルヘッダ(MAC)もダンプに表示する
  • port 8000
    • ポート8000番のパケットを確認する
  • -nn
    • ポート番号をプロトコル名に変換しない(ポート番号をそのまま表示する)

です。

 

Windowsからcurlで動作確認

Windowscurlを実行します。しばらくすると、

# curlの実行
>curl http://192.168.10.101:8000
# 実行結果
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
...
</html>

とHTTPレスポンスがありました。

 
HTTPサーバのログを見ると、アクセスがあったようです。

192.168.10.50 - - [06/Jan/2018 23:41:47] "GET / HTTP/1.1" 200 -

 
ラズパイのルータログにも出力がありました。

Ether / IP / TCP 192.168.10.50:64314 > 192.168.10.101:8000 S / Padding
Ether / IP / TCP 192.168.10.101:8000 > 192.168.11.201:64314 SA
Ether / IP / TCP 192.168.10.50:64314 > 192.168.10.101:8000 A / Padding
...

 
また、tcpdumpにも出力されていました。

14:41:46.288642 b8:xx:xx:xx:xx:xx > c4:xx:xx:xx:xx:xx, ethertype IPv4 (0x0800), length 68: 192.168.10.50.64314 > 192.168.10.101.8000: Flags [S], seq 3620418925, win 64240, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
14:41:46.357560 c4:xx:xx:xx:xx:xx > b8:xx:xx:xx:xx:xx, ethertype IPv4 (0x0800), length 66: 192.168.10.101.8000 > 192.168.10.50.64314: Flags [S.], seq 4046793993, ack 3620418926, win 65535, options [mss 1460,nop,wscale 5,sackOK,eol], length 0
14:41:46.907171 b8:xx:xx:xx:xx:xx > c4:xx:xx:xx:xx:xx, ethertype IPv4 (0x0800), length 62: 192.168.10.50.64314 > 192.168.10.101.8000: Flags [.], ack 1, win 256, length 0
...

 
これらより、NATルータ化が成功したと考えられました。

 

NAPTルータの実装

続いて、NAPTルータの実装を行います。

内容はほぼ同じですが、ローカルIPアドレスとポート番号をNAPTテーブルに保存しておくところが異なります。

なお、かなり遅い実装なため、curlタイムアウトする恐れがあります...

 

scapy_napt_router_using_ip_layer.py

# -*- coding: utf-8 -*-
from scapy.all import conf, send, get_if_hwaddr, get_if_addr
from select import select
from scapy.layers.inet import IP, TCP, UDP
import os
import random


def recreate_ip_packet(ip_packet):
    if ip_packet.haslayer(TCP):
        del ip_packet.getlayer(TCP).chksum
    if ip_packet.haslayer(UDP):
        del ip_packet.getlayer(UDP).chksum
    del ip_packet.chksum

    result = IP(str(ip_packet))
    return result


def is_ssh_packet(target_packet):
    if not target_packet.haslayer(TCP):
        return False
    tcp_layer = target_packet.getlayer(TCP)
    return tcp_layer.dport == 22


def napt_router():
    try:
        global_socket = conf.L2socket(iface='eth0')
        local_socket = conf.L2socket(iface='eth1')

        global_eth_mac_address = get_if_hwaddr('eth0')
        global_eth_ip_address = get_if_addr('eth0')
        local_eth_mac_address = get_if_hwaddr('eth1')

        os.system('sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP')

        # NAPTテーブル
        napt_table = {}

        while True:
            readable_sockets, _, _ = select([global_socket, local_socket], [], [])

            for s in readable_sockets:
                p = s.recv()

                if not p or not p.haslayer(IP):
                    continue
                if is_ssh_packet(p):
                    continue

                if p.dst == local_eth_mac_address:
                    packet_from_local_to_global = p.getlayer(IP)

                    # すでにNAPTで使用したローカルIP&ポートの場合は、以前のグローバルIP&ポートを使う
                    # IPとポートが変わってしまうと、TCPコネクションがうまくいかず、通信が途切れる
                    if (packet_from_local_to_global.src, packet_from_local_to_global.sport) \
                            in napt_table.values():
                        for global_eth_ip_address, global_port in napt_table.keys():
                            if napt_table[(global_eth_ip_address, global_port)] == \
                                    (packet_from_local_to_global.src, 
                                     packet_from_local_to_global.sport):
                                break
                    else:
                        # まだNAPTで使用していないローカルIP&ポートの場合
                        # NAPTで使う変換後のポートをランダムに取得する
                        # ポートの範囲は、プライベートポート番号とする
                        global_port = random.randint(49152, 65535)
                        # レスポンスパケットのIPアドレス・ポートを変換するため、
                        # 変換後のIPアドレス・ポートをキーとして、変換前のIPアドレス・ポートを保管しておく
                        napt_table[(global_eth_ip_address, global_port)] = \
                            (packet_from_local_to_global.src, packet_from_local_to_global.sport)
                        
                    # 送信元IPアドレスとポートを、ルーターのグローバル側IPアドレスとランダムなポートに差し替える
                    packet_from_local_to_global.src = global_eth_ip_address
                    packet_from_local_to_global.sport = global_port

                    packet_to_global = recreate_ip_packet(packet_from_local_to_global)
                    print packet_to_global.summary()
                    send(packet_to_global, verbose=0)

                elif p.dst == global_eth_mac_address:
                    packet_from_global_to_local = p.getlayer(IP)
                    # 変換後のIPアドレス・ポートから変換前IPアドレスを取得する
                    local_pc_ip_address, local_pc_port = napt_table.get(
                        (packet_from_global_to_local.dst, packet_from_global_to_local.dport))
                    # 念のため、変換できない場合はパケットを送信しない
                    if local_pc_ip_address is None or local_pc_port is None:
                        continue
                    # 宛先IPアドレス・ポートを変換前IPアドレス・ポートに差し替える
                    packet_from_global_to_local.dst = local_pc_ip_address
                    packet_from_global_to_local.dport = local_pc_port

                    packet_to_local = recreate_ip_packet(packet_from_global_to_local)
                    print packet_to_local.summary()
                    send(packet_to_local, verbose=0)

    except KeyboardInterrupt:
        pass


if __name__ == '__main__':
    napt_router()

 

NAPTルータの動作確認

NATの時と同じように

  • ラズパイにて、NAPTルータの起動
  • ラズパイにて、tcpdumpを起動
  • Windowsにて、curlを実行

したところ、うまく動作しました。

HTTPレスポンス

>curl http://192.168.10.101:8000
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
...
</html>

 
HTTPサーバのログにも

192.168.10.50 - - [06/Jan/2018 23:49:06] "GET / HTTP/1.1" 200 -

 
NAPTルータのログ

pi@raspberrypi:~/router_jisaku $ sudo python scapy_napt_router_using_ip_layer.py 
IP / TCP 192.168.10.50:57112 > 192.168.10.101:8000 S / Padding
IP / TCP 192.168.10.101:8000 > 192.168.11.201:64318 SA
IP / TCP 192.168.10.50:57112 > 192.168.10.101:8000 A / Padding
...

 
tcpdumpのログ

14:49:05.588728 b8:xx:xx:xx:xx:xx > c4:xx:xx:xx:xx:xx, ethertype IPv4 (0x0800), length 68: 192.168.10.50.57112 > 192.168.10.101.8000: Flags [S], seq 2576276753, win 64240, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
14:49:05.590540 c4:xx:xx:xx:xx:xx > b8:xx:xx:xx:xx:xx, ethertype IPv4 (0x0800), length 66: 192.168.10.101.8000 > 192.168.10.50.57112: Flags [S.], seq 1977036037, ack 2576276754, win 65535, options [mss 1460,nop,wscale 5,sackOK,eol], length 0
14:49:06.131767 b8:xx:xx:xx:xx:xx > c4:xx:xx:xx:xx:xx, ethertype IPv4 (0x0800), length 62: 192.168.10.50.57112 > 192.168.10.101.8000: Flags [.], ack 1, win 256, length 0

 

参考資料

 

ソースコード

GitHubに上げました。ディレクトscapy_python2/router/ にある

  • scapy_nat_router_using_ip_layer.py
  • scapy_napt_router_using_ip_layer.py

が今回のファイルです。
https://github.com/thinkAmi-sandbox/syakyo-router_jisaku

Scapy + Python2 + I/O多重化・ブロッキングI/O系モジュールにて、Raspberry Pi 2 Model Bをブリッジ化する

前回、Scapyの bridge_and_sniff() 関数を使って、Raspberry Pi 2 Model B をブリッジ化してみました。
Python2 + Scapyで、Raspberry Pi 2 Model B をブリッジにできるか試してみた #router_jisaku - メモ的な思考的な

ただ、 bridge_and_sniff() 関数を使うだけではお手軽すぎるため、今回は別の方法でブリッジ化してみます。

 
なお、誤った理解があれば、ご指摘いただけるとありがたいです。

 
目次

 

環境

前回の環境と同じですので、さらっと書いておきます。

  • Mac OS X 10.11.6
    • 疎通確認のために、PythonでHTTPサーバを立てる
  • Raspberry Pi 2 Model B
    • Raspbian
      • RASPBIAN STRETCH WITH DESKTOP
      • Version: November 2017
    • Python 2.7.13
      • Raspbianにインストールされていたものを使用
    • Scapy 2.3.3
    • 外付けUSB有線LANアダプタ(LUA4-U3-AGT)
    • SSHMacから接続可能
  • Windows10
    • curlをインストール済

 

bridge_and_sniff()関数の実装を見る

まずは、Scapyのbridge_and_sniff()関数の実装を見てみます。コードはこのあたりです。
https://github.com/secdev/scapy/blob/v2.3.3/scapy/sendrecv.py#L552

bridge_and_sniff()関数では、ネットワークというI/Oバウンドな処理を効果的に行えるよう、I/O多重化の select を使っていました。

処理の流れは

でした。

 

今回試してみる実装形式

I/Oバウンドを効果的に処理できそうなPythonの標準モジュールをみたところ、

  • I/O多重化: select
  • ブロッキングI/O系
    • マルチスレッド: threading
    • スレッドプール: concurrent.futures
  • ノンブロッキングI/O系
    • イベントループ(Python3.4〜): asyncio
    • Python3では後方互換維持のためだけに存在: asyncore

がありました。

Python2なScapyなこともあり、ノンブロッキングI/O系の実装は難しそうなため*1、今回はI/O多重化とブロッキングI/O系モジュールを使って実装してみます。

 
なお、I/Oバウンドな処理ではマルチプロセスやプロセスプールは効果的ではありません。
Pythonをとりまく並行/非同期の話

とはいえ、それらのコードも書いてみたかったので、

  • ブロッキングI/O系
    • マルチプロセス: multiprocess
    • プロセスプール: concurrent.futures

でも実装してみました。

 

I/O多重化:selectモジュールのselect()での実装

I/O多重化には

  • select
  • poll
  • epoll

などがあります。

 
まずは、Scapyのbridge_and_sniff()関数と同じく、selectモジュールの select() を試してみます。
16.1. select — I/O 処理の完了を待機する — Python 2.7.14 ドキュメント

scapy_bridge_select.py

# -*- coding: utf-8 -*-
from scapy.all import conf
import select


def bridge():
    try:
        # レイヤ2のソケットを用意
        eth0_socket = conf.L2socket(iface='eth0')
        eth1_socket = conf.L2socket(iface='eth1')
        # 別のインタフェースからパケットを送信するための辞書
        next_socket = {
            eth0_socket: eth1_socket,
            eth1_socket: eth0_socket,
        }
        while True:
            # select()関数で使えるようになるまで待機
            readable_sockets, _, _ = select.select([eth0_socket, eth1_socket], [], [])

            for s in readable_sockets:
                # 準備できたソケットから受信
                p = s.recv()

                if p:
                    # パケット全体をターミナルへ表示
                    p.show()

                    # 受信したパケットを別のインタフェースから送信
                    next_socket[s].send(p.original)

    except KeyboardInterrupt:
        pass


if __name__ == '__main__':
    bridge()

 
ためしにrecv()で受信した時の内容を見てみると、

# パケットの型をターミナルへ表示
print '---- packet type from recv(): {}'.format(type(p))
# => <class 'scapy.layers.l2.Ether'>
print '---- original type from recv(): {}'.format(type(p.original))
# => <type 'str'>

でした。

 

I/O多重化:selectモジュールのepoll()での実装

RaspbianはLinuxベースのOSなため、I/O多重化の epoll が使えます。

Pythonではselectモジュールの epoll() 関数を使って実装します。

scapy_bridge_epoll.py

# -*- coding: utf-8 -*-
from scapy.all import conf
import select


def bridge():
    try:
        # レイヤ2のソケットを用意
        eth0_socket = conf.L2socket(iface='eth0')
        eth1_socket = conf.L2socket(iface='eth1')

        epoll = select.epoll()
        # 読み出し可能なデータが存在する場合を登録
        # https://docs.python.jp/2/library/select.html#edge-and-level-trigger-polling-epoll-objects
        epoll.register(eth0_socket.fileno(), select.EPOLLIN)
        epoll.register(eth1_socket.fileno(), select.EPOLLIN)

        while True:
            # イベントを1秒待機
            events = epoll.poll(1)

            for fd, event in events:
                # ファイルディスクリプタがeth0のものと等しい場合
                if fd == eth0_socket.fileno():
                    p_eth0 = eth0_socket.recv()

                    if p_eth0:
                        # eth0で受信したパケットをeth1で送信
                        eth1_socket.send(p_eth0.original)

                # ファイルディスクリプタがeth1のものと等しい場合
                elif fd == eth1_socket.fileno():
                    p_eth1 = eth1_socket.recv()

                    if p_eth1:
                        # eth1で受信したパケットをeth0で送信
                        eth0_socket.send(p_eth1.original)

    except KeyboardInterrupt:
        pass


if __name__ == '__main__':
    bridge()

 
ためしに、epoll.poll()で取得したfdとeventの値を見ると、

print '--- fd type: {}'.format(type(fd))
# => fd type: <type 'int'>
print '--- event type: {}'.format(type(event))
# => event type: <type 'int'>

でした。

 

マルチスレッド:threadingモジュールでの実装

今度はマルチスレッドの threading モジュールを使ってみます。

 
threadingモジュールを使う場合、 Ctrl +C で終了できなくなりますが、今回はデーモンスレッドで動かすことで回避しました。

 
ただ、デーモンスレッドの場合、

注釈 デーモンスレッドは終了時にいきなり停止されます。デーモンスレッドで使われたリソース (開いているファイル、データベースのトランザクションなど) は適切に解放されないかもしれません。きちんと (gracefully) スレッドを停止したい場合は、スレッドを非デーモンスレッドにして、Event のような適切なシグナル送信機構を使用してください。

https://docs.python.jp/3/library/threading.html#thread-objects

に注意します。今回は関係なさそうなので、気にしないことにします。

scapy_bridge_threading.py

# -*- coding: utf-8 -*-
from scapy.all import conf
import threading


def bridge_from_eth0_to_eth1():
    eth0_socket = conf.L2socket(iface='eth0')
    eth1_socket = conf.L2socket(iface='eth1')

    while True:
        p = eth0_socket.recv()
        if p:
            eth1_socket.send(p.original)


def bridge_from_eth1_to_eth0():
    eth0_socket = conf.L2socket(iface='eth0')
    eth1_socket = conf.L2socket(iface='eth1')

    while True:
        p = eth1_socket.recv()
        if p:
            eth0_socket.send(p.original)


def bridge():
    try:
        # スレッドを用意
        bridge_eth0 = threading.Thread(target=bridge_from_eth0_to_eth1)
        bridge_eth1 = threading.Thread(target=bridge_from_eth1_to_eth0)

        # 今回はいきなり止まっても問題ないため、デーモンモードで動くようにする
        # https://docs.python.jp/3/library/threading.html#thread-objects
        bridge_eth0.daemon = True
        bridge_eth1.daemon = True

        # スレッドを開始
        bridge_eth0.start()
        bridge_eth1.start()

        # KeyboardInterruptを受け付けるよう、join()を秒指定で使う
        bridge_eth0.join(5)
        bridge_eth1.join(5)

    except KeyboardInterrupt:
        pass


if __name__ == '__main__':
    bridge()

 

マルチプロセス:multiprocessでの実装

次はマルチプロセスにて実装します。

 
繰り返しとなりますが、I/Oバウンドではマルチプロセス化しても効果的な処理になりません。今回は書いてみたかったという理由だけで実装しています。

scapy_bridge_multiprocess.py

# -*- coding: utf-8 -*-
from scapy.all import conf
import multiprocessing


def bridge_from_eth0_to_eth1():
    eth0_socket = conf.L2socket(iface='eth0')
    eth1_socket = conf.L2socket(iface='eth1')

    while True:
        p = eth0_socket.recv()
        if p:
            eth1_socket.send(p.original)


def bridge_from_eth1_to_eth0():
    eth0_socket = conf.L2socket(iface='eth0')
    eth1_socket = conf.L2socket(iface='eth1')

    while True:
        p = eth1_socket.recv()
        if p:
            eth0_socket.send(p.original)


def bridge():
    try:
        # プロセスを用意
        bridge_eth0 = multiprocessing.Process(target=bridge_from_eth0_to_eth1)
        bridge_eth1 = multiprocessing.Process(target=bridge_from_eth1_to_eth0)

        # プロセスを開始
        bridge_eth0.start()
        bridge_eth1.start()

    except KeyboardInterrupt:
        # threadingと異なり、特に何もしなくてもCtrl + C が可能
        pass


if __name__ == '__main__':
    bridge()

 

スレッドプール:concurrent.futures.ThreadPoolExecutorでの実装

Python3.2より登場した concurrent.futures モジュールの ThreadPoolExecutor を使ってスレッドプール版を実装します。
17.4. concurrent.futures – 並列タスク実行 — Python 3.6.3 ドキュメント

ただ、Python2では標準モジュールに入っていないため、バックポートされたモジュール futuresPyPIからインストールする必要があります。
https://pypi.python.org/pypi/futures

pi@raspberrypi:~/router_jisaku $ sudo pip install futures
Collecting futures
  Using cached futures-3.2.0-py2-none-any.whl
Installing collected packages: futures
Successfully installed futures-3.2.0

 
実装です。

scapy_bridge_ThreadPoolExecutor.py

# -*- coding: utf-8 -*-
from scapy.all import conf
import concurrent.futures


def bridge_from_eth0_to_eth1():
    eth0_socket = conf.L2socket(iface='eth0')
    eth1_socket = conf.L2socket(iface='eth1')

    while True:
        p = eth0_socket.recv()
        if p:
            eth1_socket.send(p.original)


def bridge_from_eth1_to_eth0():
    eth0_socket = conf.L2socket(iface='eth0')
    eth1_socket = conf.L2socket(iface='eth1')

    while True:
        p = eth1_socket.recv()
        if p:
            eth0_socket.send(p.original)


def bridge():
    try:
        # スレッドを用意
        executor = concurrent.futures.ThreadPoolExecutor(max_workers=3)
        executor.submit(bridge_from_eth0_to_eth1)
        executor.submit(bridge_from_eth1_to_eth0)

    except KeyboardInterrupt:
        # threadingと異なり、特に何もしなくてもCtrl + C が可能
        pass


if __name__ == '__main__':
    bridge()

 

プロセスプール:concurrent.futures.ProcessPoolExecutorでの実装

こちらもバックポートされたモジュール futures を使います。

ProcessPoolExecutorでの実装ですが、

  • I/Oバウンドでは効果的な処理にならない
  • PyPIにある注釈によると、クリティカルなところでは使うべきでないとのこと

より、マルチプロセス版同様、実装してみたかっただけです。

scapy_bridge_ProcessPoolExecutor.py

# -*- coding: utf-8 -*-
from scapy.all import conf
import concurrent.futures


def bridge_from_eth0_to_eth1():
    eth0_socket = conf.L2socket(iface='eth0')
    eth1_socket = conf.L2socket(iface='eth1')

    while True:
        p = eth0_socket.recv()
        if p:
            eth1_socket.send(p.original)


def bridge_from_eth1_to_eth0():
    eth0_socket = conf.L2socket(iface='eth0')
    eth1_socket = conf.L2socket(iface='eth1')

    while True:
        p = eth1_socket.recv()
        if p:
            eth0_socket.send(p.original)


def bridge():
    try:
        # スレッドを用意
        executor = concurrent.futures.ThreadPoolExecutor(max_workers=3)
        executor.submit(bridge_from_eth0_to_eth1)
        executor.submit(bridge_from_eth1_to_eth0)

    except KeyboardInterrupt:
        # threadingと異なり、特に何もしなくてもCtrl + C が可能
        pass


if __name__ == '__main__':
    bridge()

 

参考資料

 

ソースコード

GitHubに上げました。ディレクトscapy_python2/bridge/ にある

  • scapy_bridge_select.py
  • scapy_bridge_epoll.py
  • scapy_bridge_threading.py
  • scapy_bridge_multiprocess.py
  • scapy_bridge_ThreadPoolExecutor.py
  • scapy_bridge_ProcessPoolExecutor.py

が今回のファイルです。
https://github.com/thinkAmi-sandbox/syakyo-router_jisaku

*1:正確には、 asyncioにはバックポートプロジェクトのTrolliusがあります。ただ、実装でハマったときに大変そうなので、今回実装するのはやめておきます。https://github.com/vstinner/trollius

Python2 + Scapyで、Raspberry Pi 2 Model B をブリッジにできるか試してみた #router_jisaku

以前、Windows10 + Scapyを簡単にさわってみました。
Windows10にScapyをインストールする - メモ的な思考的な

 
もう少し詳しくScapyをさわってみたいと思い、何か作ろうと考えました。

作る題材を探していたところ、書籍「ルーター自作でわかるパケットの流れ」に出会いました。
ルーター自作でわかるパケットの流れ ~ソースコードで体感するネットワークのしくみ:書籍案内|技術評論社

ルーター自作でわかるパケットの流れ

ルーター自作でわかるパケットの流れ

 
書籍ではC言語を使ってパケットを扱い、ブリッジやルーターを自作していました。パケットを扱うならScapyでも書けるのではと思いました。

そこで、Python2 + Scapyを使って、Raspberry Pi 2 Model B をブリッジにできるか試してみました。

 
目次

 

環境

今回は 192.168.10.0/24 のローカル環境にて

  • Mac = HTTPサーバ
  • 無線LANアクセスポイント = スイッチ
  • ラズパイ = ブリッジ
  • Windows10 = ブリッジにぶら下げたクライアント

の役割を持たせました。

 

ネットワーク構成
-----------------------------------------
Mac
(`en0` : WiFi)
-----------------------------------------
|
|
-----------------------------------------
(無線)
無線LANアクセスポイント
(スイッチ)
-----------------------------------------
|
|
-----------------------------------------
(`eth0` : オンボードLANアダプタ)
Raspberry Pi 2 Model B
(`eth1` : 外付けUSB有線LANアダプタ)
-----------------------------------------
|
|
-----------------------------------------
(オンボードLANアダプタ)
Windows10
-----------------------------------------

 

ネットワークアダプタ設定

明示的に設定したネットワークアダプタ設定です。

なお、以降の表記は

とします。

機器 アダプタ IPアドレス DGW DNS
Mac en0 192.168.10.101/24 192.168.10.1 192.168.10.1
無線LAN AP 192.168.10.1/24
ラズパイ eth0 192.168.10.50/24 192.168.10.1
ラズパイ eth1
Windows 192.168.10.201/24 192.168.10.1 192.168.10.1

 

各機器の構成

 
なお、Scapyは2つあります。

ただ、何かあった時に

  • 自分の書き方が悪い
  • scapy-python3の非互換なところ

の区別がつかないため、今回はPython2版のScapyを使いました。

 

ラズパイのセットアップ

以前ラズパイは使用していましたが、2015/7以降使っていなかったため、改めてラズパイをセットアップします。

 

メモリカードの調達

以前は、bootとrootを分けたファイルシステムを使っていました。
Raspberry Pi 2 + Raspbianで、microSDをboot、USBメモリをrootというファイルシステムにする - メモ的な思考的な

ただ、セットアップするのがめんどうなので、相性の良さそうなメモリカードを調達することにしました。

安価で入手しやすく動作報告の多いものを調べたところ、東芝MSDAR40N08G が良さそうでした。

年末年始だったので家電量販店に行きましたが、普通に売っていました。

 

Raspbianのインストール

公式のインストールガイドに従い、Raspbianをインストールしました。
Installing operating system images - Raspberry Pi Documentation

  • RASPBIAN STRETCH WITH DESKTOP のzipファイルをダウンロード
  • MacUSBメモリカードリーダーを接続し、Etcher でzipファイルをmicroSDに書き込む

 

Raspbianの初期セットアップ

ラズパイに microSDを挿入し、外付けキーボード・HDMI接続のモニタ・LANケーブル・外付けUSB有線LANアダプタをつないで、GUIで初期セットアップします。

USBマウスは手元に無かったものの、キーボード操作で何とかなりました。

 

キーボードレイアウトの変更

OADG 109A を選びました。

 

vimのインストール
pi@raspberrypi:~ $ sudo apt-get update
pi@raspberrypi:~ $ sudo apt-get install vim

 

ネットワークアダプタ固定IPアドレス割り当て

今回はブリッジなため、ラズパイのネットワークアダプタにはIPアドレスがいらない気がしました。

ただ、SSHを使ってMac上のターミナルからPythonコードを書きたいため、ラズパイのネットワークアダプタIPアドレスを割り当てます。

設定前のラズパイのネットワークアダプタを見ると

pi@raspberrypi:~ $ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
...

eth1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
...

でした。

今回は、ラズパイのオンボードLANアダプタをMacに接続するため、 eth0固定IPアドレスを割り当てます。

 
以前ラズパイに固定IPアドレスを割り当てた時は、 /etc/network/interfaces に追加していました。
Raspberry Pi 2 Model BにRaspbianをセットアップした - メモ的な思考的な

ただ、現在はDebian Stretch (Linux 4.9 カーネル)がベースなため、設定ファイルが /etc/dhcpcd.conf に変更となっています。

pi@raspberrypi:~ $ vi /etc/dhcpcd.conf

# 末尾に以下を追加
interface eth0
static ip_address=192.168.10.50/24
static routers=192.168.10.1
static domain_name_servers=192.168.10.1 

 
設定を反映・確認します。

# 設定を反映
pi@raspberrypi:~ $ sudo service dhcpcd reload
sending signal HUP to pid 323

# 設定内容を確認
pi@raspberrypi:~ $ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.10.50  netmask 255.255.255.0  broadcast 192.168.10.255

 

SSHを有効化

raspi-config にて有効化します。

pi@raspberrypi:~ $ sudo raspi-config

5 Interfacing Options > P2 SSH にて enabled にします。

 
MacからラズパイにSSHしてみましたが、問題なく動作しました。  

$ ssh pi@192.168.10.50
pi@192.168.10.50's password: (raspberryと入力)
Linux raspberrypi 4.9.59-v7+ #1047 SMP Sun Oct 29 12:19:23 GMT 2017 armv7l

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Mon Jan  1 23:37:50 2018 from 192.168.10.101

SSH is enabled and the default password for the 'pi' user has not been changed.
This is a security risk - please login as the 'pi' user and type 'passwd' to set a new password.

pi@raspberrypi:~ $ 

 

Scapyのインストール

ラズパイでScapyを実行する場合、 sudo python <scapyのコード> とします。

そのため、Scapyはsudoでインストールします。

# pythonのバージョンを確認
pi@raspberrypi:~ $ sudo python --version
Python 2.7.13

# Scapyのインストール
(env) pi@raspberrypi:~ $ sudo pip install scapy
Collecting scapy
  Downloading scapy-2.3.3.tgz (1.4MB)
    100% |████████████████████████████████| 1.4MB 84kB/s 
Building wheels for collected packages: scapy
  Running setup.py bdist_wheel for scapy ... done
  Stored in directory: /root/.cache/pip/wheels/bd/cf/05/d5abc9b4434f39ffe231517dfb8dab96241fef6a99459051f9
Successfully built scapy
Installing collected packages: scapy
Successfully installed scapy-2.3.3

以上でラズパイのセットアップは完了です。

 

疎通確認の準備

MacでローカルHTTPサーバを起動

ブリッジを介しても通信できるか確認するため、MacPythonのHTTPサーバを起動しておきます。

# Python2の場合
$ python -m SimpleHTTPServer

# Python3の場合
$ python -m http.server

 

Windowsから通信できないことを確認

WindowsからMacPython HTTPサーバへcurlを実行し、通信できないことを確認します。

> curl -m5 http://192.168.10.101:8000
curl: (28) Connection timed out after 5015 milliseconds

 

Scapyの「bridge_and_sniff」関数でラズパイをブリッジ化

ここまででラズパイの準備ができました。次はラズパイをScapyでブリッジにしてみます。

Scapyで便利な関数がないかを探したところ、 scapy/scapy/sendrecv.pybridge_and_sniff() 関数がありました。
https://github.com/secdev/scapy/blob/v2.3.3/scapy/sendrecv.py#L638

 
そこで、ラズパイ上に以下のファイルを作成し、ブリッジにできるかを試してみます。

scapy_bridge_and_sniff.py

# -*- coding: utf-8 -*-
from scapy.sendrecv import bridge_and_sniff


if __name__ == '__main__':
    print '>----- enable bridge -----'
    bridge_and_sniff('eth0', 'eth1')
    print '<----- disable bridge -----'

 
続いて、上記のファイルを実行します。rootでないと動作しないため、 sudo を忘れずに行います。

pi@raspberrypi:~/router_jisaku $ sudo python scapy_bridge_and_sniff.py
>----- enable bridge -----

 
動作確認のため、Windowsからcurlを実行します。

なお、今回はタイムアウトを5秒( -m 5 ) としました。ただ、通信状況によっては5秒以内には完了しないため、必要に応じてタイムアウトを延長します。

>curl -m5 http://192.168.10.101:8000
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
...
</html>

HTMLが返ってきました。通信できたようです。

 
Mac上のHTTPサーバのログにも通信ログがありました。

192.168.10.201 - - [02/Jan/2018 10:30:59] "GET / HTTP/1.1" 200 -

 
スクリプト scapy_bridge_and_sniff.py を停止した時の挙動を見てみます。

ラズパイにて Ctrl + Cスクリプトを停止します。

pi@raspberrypi:~/router_jisaku $ sudo python scapy_bridge_and_sniff.py
>----- enable bridge -----
^C<----- disable bridge -----

 
Windowscurlで確認します。

>curl -m5 http://192.168.10.101:8000
curl: (28) Connection timed out after 5000 milliseconds

接続できなくなっていました。

 
以上より、Python2 + Scapyで、Raspberry Pi 2 Model B をブリッジにできました。

ただ、今回はScapyの bridge_and_sniff() 関数を使うだけだったので、もう少し他の実装を考えてみます。

 

ソースコード

GitHubに上げました。scapy_python2/bridge/scapy_bridge_and_sniff.py が今回のファイルです。
https://github.com/thinkAmi-sandbox/syakyo-router_jisaku

2017年の振り返りと2018年の目標

例年通り、2017年の振り返りと2018年の目標を書いてみます。

 

目標の振り返り

2016年の振り返りと2017年の目標 - メモ的な思考的な で立てた目標を振り返ってみます。

 

Pythonの基礎知識を身につける

一年間Pythonの仕事であったため、仕事で使う部分を中心に基礎知識が増えました。

ただ、Pythonの世界はまだまだ広いので、これに安住することなく手を動かし続けないとマズイとも感じました。

 

MacLinuxに慣れる

2017年はターミナル生活が長かったこともあり、日常生活で困らない程度には慣れてきました。

 

生活リズムを整える

転職後一年以上経過したこともあり、日々の生活リズムが整ってきました。

朝型生活になるかなと思いましたが、意外と夜の時間も使っていました。

 

その他

GitHub

f:id:thinkAmi:20171231224719p:plain

 
初めてのプルリク&マージも経験しました。
初めてのOSSへのプルリクとマージ - メモ的な思考的な

 

イベント

2017年もいろいろと参加していました。

  1. デブサミ2017の二日目に参加しました #devsumi - メモ的な思考的な
  2. #stapy #glnagano 第15回みんなのPython勉強会リモート中継 in GEEKLAB.NAGANO に参加しました - メモ的な思考的な
  3. #jawsdays JAWS DAYS 2017に参加しました - メモ的な思考的な
  4. #stapy #glnagano みんなのPython勉強会 in 長野#1に参加しました - メモ的な思考的な
  5. #stapy #glnagano みんなのPython勉強会リモート中継 in GEEKLAB.NAGANO#16 に参加しました - メモ的な思考的な
  6. オープンハードカンファレンス2017 Naganoに参加しました - メモ的な思考的な
  7. Google I/O 2017 Extended Live Viewing 信州に参加しました - メモ的な思考的な
  8. #pyconjp PyCon JP 2017に参加しました - メモ的な思考的な
  9. #stapy 第29回みんなのPython勉強会に参加しました & LTしました - メモ的な思考的な

 
また、縁あってPython Boot CampにTAとして2回参加しました。

  1. #pycamp #glnagano #nseg Python Boot Camp in 長野にTAとして参加しました - メモ的な思考的な
  2. #pycamp Python Boot Camp in 長野八ヶ岳にTAとして参加しました - メモ的な思考的な

 
その他、ほとんど記録していませんでしたが、平日の読書会にも参加しました。

  1. 第8回 SQLアンチパターン読書会に参加しました - メモ的な思考的な
  2. #nseg 第7回「オブジェクト指向設計実践ガイド」読書会に参加しました - メモ的な思考的な

 

アドベントカレンダー

Robot Framework のアドベントカレンダーを立てました & 書きました。
Robot Framework Advent Calendar 2017

アドベントカレンダー用に調べたり、自分以外に執筆してくださった方のネットワークネタが参考になったりと、立てて良かったです。

 

2018年の目標っぽいもの

今のところPython中心の生活なため、

  • Pythonを通じて、いろいろな分野の基礎固め

を目標っぽいものにします。

素振りして、何かしらの形としてアウトプットできればと。

 
というところで、今年もよろしくお願いします。