Python2 + nfcpyで、W525DZのFeliCa Plugを読んでみた

毎年冬になるとつらいのが冷え性です。

今までは身体が冷えてるような気がするから冷え性だろうと思っていました。しかし、「推測するな計測せよ」という言葉を思い出しました。

そこで記録が手軽に残せる体温計を探してみました。すると、NFCでデータを転送し、専用アプリでデータを管理できる体温計がありました。
WOMAN℃ テルモ女性体温計W525DZ|女性体温計 |テルモ 一般のお客様向け情報

目的が違うような気もしますが、データを残しておきたい気持ちが勝ったため、入手しました。

 
残念なのは専用アプリがWindows向けであり、Macでは使えないことです。

もしかしたら nfcpy を使えばデータを読めるのではないかと思い、試してみることにしました。
nfcpy/nfcpy: A Python module to read/write NFC tags or communicate with another NFC device.

 
結論からすると、Macでもデータは読み込めたものの、データのフォーマットが公開されていない、もしくは、暗号化された領域にデータを保存しているため、うまくいきませんでした。

とはいえ、せっかくなので、読み込んだデータなどをメモしておきます。

 
目次

 

環境

なお、nfcpyはPython3対応を進めているようです。進捗状況は以下のissueにありました。
Support Python 3 · Issue #47 · nfcpy/nfcpy

 

実装内容

NFCタグとの対話をハンドリングする

NFCタグとの対話をハンドリングする方法は、以下にありました。
Read and write tags | Getting started — nfcpy 0.13.4 documentation

ContactlessFrontend は使い終わったら close() する必要がありますが、Pythonのwith文に対応しているため、以下のように書けます。

with nfc.ContactlessFrontend('usb') as clf:
    clf.connect(rdwr={'on-connect': connected})

 
あとは、 connected() コールバック関数を定義し、その中にNFCタグとの通信を記載すれば良さそうです。

コールバック関数には引数tagが渡されてくるため、その中身を見てみました。IDやPMMはボカしていますが、こんな感じでした。  

def connected(tag):
    print tag
    # => Type3Tag 'FeliCa Plug (RC-S926)' ID=03xxxxxxxxxxxxxx PMM=01xxxxxxxxxxxxxx SYS=FEE1

    print type(tag)
    # => <class 'nfc.tag.tt3_sony.FelicaPlug'>

    print dir(tag)
    # =>
    # ['IC_CODE_MAP', 'NDEF', 'TYPE', '__class__', '__delattr__', '__dict__', '__doc__',
    #  '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__',
    #  '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
    #  '__subclasshook__', '__weakref__', '_authenticated', '_clf', '_format', '_is_present',
    #  '_ndef', '_nfcid', '_product', '_target', 'authenticate', 'clf', 'dump', 'dump_service',
    #  'format', 'identifier', 'idm', 'is_authenticated', 'is_present', 'ndef', 'pmm', 'polling',
    #  'product', 'protect', 'read_from_ndef_service', 'read_without_encryption',
    #  'send_cmd_recv_rsp', 'sys', 'target', 'type', 'write_to_ndef_service',
    #  'write_without_encryption']

 
この結果より、

  • システムコードが FEE1
  • 引数tagが FelicaPlug

であることから、普通のFeliCaではなく、W525DZではFeliCa Plug を使っているようでした。

 

Polling

続いて、pollingを試してみます。

nfcpyの関数 polling() の引数 system_code にはシステムコードが必要そうです。

システムコードは tag.sys に設定されていることから、それを使います。

def connected(tag):
    print tag.polling(tag.sys)
    # => (bytearray(b'\x03\xxx\xxx\xxx\xxx\xxx\xxx\xxx'),
    #     bytearray(b'\x01\xxx\xxx\xxx\xxx\xxx\xxx\xxx'))

先ほど tagをprintしたときに出てきた値を、bytearrayのタプルとして取得できました。

 

Read Without Encryption

さらに、暗号化されていない部分からデータを読み込んでみます。

今回は、tagオブジェクトの read_without_encryption() を使えば良さそうでした。

引数は2つあり、サービスコードのリストとブロックコードのリストでした。

サービスコードは nfc.tag.tt3.ServiceCode クラスのインスタンスを渡せば良さそうでした。

必要な値は、FeliCa Plug ユーザーズマニュアル v1.14の

  • p47 より、サービスコードリスト順番は 0
  • p56 より、サービスコードは 000Bh
  • p55 より、サービス数は 1

と推定したため、

sc = nfc.tag.tt3.ServiceCode(0, 0x0b)

の1要素を持つリストとしました(とはいえ、第一引数 number がサービスコードリスト順番なのかは自信がないですが...)。

 
ブロックコードについては nfc.tag.tt3.BlockCode クラスのインスタンスを使います。

コンストラクタの引数については、

  • number : ブロック番号
  • service : サービスコードリストのindex (今回は1つしかないので 0 )

を指定したのを2つ用意しました。

bc1 = nfc.tag.tt3.BlockCode(0, service=0)
bc2 = nfc.tag.tt3.BlockCode(1, service=0)

 
あとはそれらを read_without_encryption() メソッドに渡し、取得したデータを binascii.hexlify() で16進数表記したのをprintしてみました。

data = tag.read_without_encryption([sc], [bc1, bc2])
print '{}'.format(binascii.hexlify(data))
# => 02fd8c73947acef9742874c2b8429cc1d7dab10accf72bd5318863f862dc0371

 
何らかの値は取れているようですが、どの位置の数字が何を意味しているのか分からないため、解読できませんでした。

 

dump()

他にメソッドがないかを探したところ、 dump() があったため試してみました。

print tag.dump()
# => ['This is not an NFC Forum Tag.']

残念ながら、FeliCa Plugでは使えないようです。

 

dump_service()

他にもダンプできそうなメソッドとして dump_service() があったため、試してみました。

引数にはサービスコードが必要そうでしたが、先ほど作成したサービスコードを流用しました。

sc = nfc.tag.tt3.ServiceCode(0, 0x0b)

print tag.dump_service(sc)
# =>
# ['0000: 02 fd 8c 73 94 7a ce f9 74 28 74 c2 b8 42 9c c1 |...s.z..t(t..B..|',
#  '*     02 fd 8c 73 94 7a ce f9 74 28 74 c2 b8 42 9c c1 |...s.z..t(t..B..|',
#  '6962: 02 fd 8c 73 94 7a ce f9 74 28 74 c2 b8 42 9c c1 |...s.z..t(t..B..|']

dump_service() はサービスに対応する全データブロックをダンプするとのことで、実行が終わるまで少々待ちました。

データは出力されたものの、こちらもフォーマットが不明なため、解読できませんでした。

 
ここまでで、フォーマットが分からないことには解読できなさそうと考え、これ以上の追求はやめました。

 

参考

 
また、ユーザーズマニュアル中の数値については、ユーザーズマニュアル(v1.14)のp3に記載がありました。

ソースコード

GitHubに上げました。 felica_plug/read_w525.py が今回のソースコードです。
https://github.com/thinkAmi-sandbox/nfcpy-sample

Python2 + Scapyでマジックパケットを作成し、Wake on LANをしてみた

今まで、遠隔からPCの電源を入れる場合、Wake on LAN 用のツールを使っていました。
Wake-on-LAN - Wikipedia

 
そこで今回、ScapyでWake on LANマジックパケットを作ってみて、ツールの代替となるかを試してみました。

 

目次

 

環境

ネットワーク構成
-----------------------------------------
Raspberry Pi 2 Model B
(`eth0` : オンボードLANアダプタ)
-----------------------------------------
|
|
-----------------------------------------
スイッチングハブ
-----------------------------------------
|
|
-----------------------------------------
(外付けUSB有線LANアダプタ)
Windows10
(オンボードLANアダプタ)
-----------------------------------------

 

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

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

なお、以降の表記は

とします。

機器 アダプタ IPアドレス DGW DNS
ラズパイ eth0 192.168.10.50/24 192.168.10.1 192.168.10.1
X201s USB有線 192.168.10.201/24 192.168.10.1 192.168.10.1
X201s オンボード DHCP DHCP DHCP

 

各機器の構成

以前、ブリッジやルータを自作した時と同じ構成です。

 

Wake on LANができる環境を調査

Wake on LANができる環境を調査するため、いろいろと試してみましたので、それらのメモを残します。

 

試してみたこと
[NG] Macからマジックパケットを送信できるか

当初MacからWiFi経由でマジックパケットを送信しようとしましたが、手元の環境が良くないのか、MacからX201sへのWake on LANは成功しませんでした。

そのため、今回はMacからマジックパケットを送信するのはあきらめました。

 

[NG] Wake on LANでラズパイを起動できるか

Wake on LANでラズパイを起動してみようと思いました。

しかし、以下にある通り、Wake on LANではラズパイは起動しないことが分かりました。

 
そのため、Wake on LANでラズパイを起動することもあきらめました。

 

[NG] Wake on LAN + オンボードLANアダプタで、Windows10のX201sを起動できるか

元々、X201sではWake on LANができそうでした。ただ、Windows10のX201sで試してみたところ、起動しませんでした。

そのため、Windows10版向けのLANアダプタドライバを探してみました。

すると、LenovoではX201sがEOL扱いとなっており、Windows10版のドライバ提供はありませんでした。
Device Drivers File Matrices - ThinkPad X201s

 
そのため、Windows10のX201sのオンボードLANアダプタを使ってWake on LANすることもあきらめました。

 

[OK] X201s + 外付けUSB有線LANアダプタで起動するか

X201sに外付けUSB有線LANアダプタのLUA4-U3-AGTをつないでみたところ、アダプタのプロパティに Wake on LAN関係の設定がありました。

そこで、それらの設定を有効化後、X201sのLUA4-U3-AGT宛にマジックパケットを送信したところ、Wake on LANができました。

そのため、

の構成で試すことにしました。

 

Wake on LANするための設定

今回、LUA4-U3-AGTでWake on LANするため、LUA4-U3-AGTの アダプターのプロパテイ > ネットワークタブの構成ボタン > 電源の管理タブ より

  • このデバイスで、コンピューターのスタンバイ状態を解除できるようにする
  • Magic Packet でのみ、コンピューターのスタンバイ状態を解除できるようにする

の2つにチェックを入れました。

 
一方、オンボードのLANアダプタではなかったせいか、

  • BIOSWake on LAN設定を無効
    • Config > Network > Wake On LAN[Disabled] にする
  • 高速スタートアップを有効化
    • コントロールパネル > 電源オプション > システム設定(電源ボタンの動作を選択する) > 現在利用可能ではない設定を変更します > 高速スタートアップを有効にする のチェックを入れたまま

としても、Wake on LANができました。

 
ただ、スリープ状態の時はWake on LANできましたが、電源OFFの時はできませんでした。

とはいえ、今回はWake on LANができればよかったので、電源OFFの時の対応は気にしないことにしました。

 

Scapyでマジックパケットを送信する

マジックパケットのフォーマットについて

以下を参考にしました。

 

ScapyでUDPデータを設定する方法について

マジックパケットを送信するにはUDPでデータを送信する必要があります。

そのため、方法を調べてみたところ、Rawクラスを使い、データ部分は decode('hex') すれば良さそうでした。
scapy command for defining the data part of udp packet - Stack Overflow

 
なお、 decode('hex') はPython2系のみ利用可能ですが、今回使うScapyはPython2なため、気にしないことにしました。
How to create python bytes object from long hex string? - Stack Overflow

 

ソースコード全体

wake_on_lan.py

# -*- coding: utf-8 -*-
# from scapy.allでも良い
from scapy.sendrecv import sendp
from scapy.layers.l2 import Ether
from scapy.layers.inet import IP, UDP, Raw


# 必要に応じて、宛先のMACアドレスに変更する
TARGET_MAC_ADDRESS = '88:xx:xx:xx:xx:xx'

PREFIX_PAYLOAD = 'ff' * 6
BROADCAST_MAC_ADDRESS = 'FF:FF:FF:FF:FF:FF'
LIMITED_BROADCAST_IP_ADDRESS = '255.255.255.255'


def send_magic_packet():
    # マジックパケットの仕様に従い、文字列でペイロードを準備
    str_payload = PREFIX_PAYLOAD + (TARGET_MAC_ADDRESS.replace(':', '') * 16)
    hex_payload = str_payload.decode('hex')

    ether_layer = Ether(dst=BROADCAST_MAC_ADDRESS)
    ip_layer = IP(dst=LIMITED_BROADCAST_IP_ADDRESS)
    udp_layer = UDP()
    raw_layer = Raw(load=hex_payload)
    magic_packet = ether_layer / ip_layer / udp_layer / raw_layer

    # ブリッジやルータと異なり、戻りパケットの受け取りは不要
    # そのため、sendp()関数でL2パケットを送信するだけで良い
    sendp(magic_packet)


if __name__ == '__main__':
    send_magic_packet()

 

実行結果

X201sをスリープにした後、上記スクリプトを実行してみたところ、

pi@raspberrypi:~/scapy_sample/wol $ sudo python wake_on_lan.py
.
Sent 1 packets.

X201sが起動しました。

これにより、Scapyでマジックパケットが正しく作成・送信できたと分かりました。

 

ソースコード

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

Mac + Python2 + Scapyで、使用中のIPアドレスを探してみた

以前、Windowsにて現在のIPアドレス利用状況を知りたい時は、以下の記事のようにしていました。
Windowsで、使用中のIPアドレスを調査する:Tech TIPS - @IT

 
Scapyを使えばARPパケットだけで同じことができるのではと思い、試してみました。

 
目次

 

環境

 

実装

ScapyでL2パケットであるARPを送信する関数を調べたところ、

  • scapy.sendrecv.sendp()
    • L2パケットの送信
  • scapy.sendrecv.srp()
    • L2パケットの送受信
  • scapy.sendrecv.srp1()
    • L2パケットの送受信(1つめのパケットのみ取り出す)
  • scapy.layers.l2.arping()
    • Arpingする

などがありました。

 
今回の目的は使用中のIPアドレスの調査なため、 arping() を使って実装すれば良さそうです。

discover_ip_address_in_use.py  

# -*- coding: utf-8 -*-
from datetime import datetime
from scapy.layers.l2 import ARP, arping


def discover():
    ip = '192.168.10.*'
    print 'start: {}'.format(datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
    answers, _ = arping(ip, timeout=1, verbose=0)

    for send_packet, recieve_packet in answers:
        print 'MAC Address: {}, IP Address: {}'.format(
            recieve_packet[ARP].hwsrc,  # MACアドレス
            recieve_packet[ARP].psrc,   # IPアドレス
        )
    print 'end  : {}'.format(datetime.now().strftime('%Y/%m/%d %H:%M:%S'))


if __name__ == '__main__':
    discover()

 
実行してみると、

$ python arp/discover_ip_address_in_use.py 
start: 2018/01/11 22:44:18
MAC Address: a4:xx:xx:xx:xx:xx, IP Address: 192.168.10.1
MAC Address: b8:xx:xx:xx:xx:xx, IP Address: 192.168.10.50
end  : 2018/01/11 22:44:29

と、結果が返ってきました。

 
なお、公式ドキュメントにもArpingについて記載されていました。
ARP Ping | Usage — Scapy 2.3.3-dev documentation

また、arping()の実装を見たところ、内部で srp() を使っていました。
https://github.com/secdev/scapy/blob/v2.4rc2/scapy/layers/l2.py#L492

 

参考

 

ソースコード

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

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