Pythonでプロミスキャスモードを実装してみた

最近、PythonでSocketプログラミングすることにハマっていて、Pythonでプロミスキャス・モードで通信をキャプチャしたいなぁと思っていました。まあ、サードパーティのライブラリを使用すれば簡単に実装できるのですが、今回はそれらに頼らない方向で実装してみることにしました。
実際に方法を調べていると、標準ライブラリのfcntlのioctlメソッドで実装している例がいくつかありました。その１つが [python-sniffer/snifferCore.py at master · zeigotaro/python-sniffer](https://github.com/zeigotaro/python-sniffer/blob/master/snifferCore.py) です。
でも、この方法めんどくね？w
C言語だったら、まずこの方法はとらないと思います。setsockoptとpacket_mreq構造体で実装するでしょう。

ということで、fcntlではなくstructを使用して、C言語ライクな実装をしてみました！

ちなみに[structライブラリ](https://docs.python.jp/3/library/struct.html#format-characters)とは、C言語で定義されている構造体をPythonでも扱えるように変換してくれる優れものです。

# 環境
- Ubuntu 18.04
- Python 3.6

Unix環境に依存している関数を使用している部分があるので、Windowsでは正常動作しません。気が向いたらWindows対応させます。

# 実装
プロミスキャス・モードの実装にはPythonの標準ライブラリのみを使用しているので、パッケージの追加インストールは必要ありません。socketとstructの２つの標準ライブラリがインポートされていればOKです。

## バイト列オブジェクトの作成
C構造体を使用してバイト列オブジェクトを作成します。これは、後に出でくるsetsockopt関数で使用します。

使用するC構造体の定義は`linux/if_packet.h`で以下のようにされています。

```c

struct packet_mreq {
	int		mr_ifindex;
	unsigned short	mr_type;
	unsigned short	mr_alen;
	unsigned char	mr_address[8];
};
```

|変数|説明|
|:--:|:--|
|mr_ifindex|プロミスキャス・モードを有効化させるインターフェース番号|
|mr_type|実行するアクション (PACKET_MR_MULTICAST、PACKET_MR_PROMISC etc)|
|mr_alen|アドレス長|
|mr_address|MACアドレス|

プロミスキャス・モードで使用するのは、`mr_ifindex`と`mr_type`です。

上記の構造体を基に、structライブラリでバイト列オブジェクトを作成します。

```python

import struct

iface = 'lo'    # loopbackインターフェース
PACKET_MR_PROMISC = 1    # プロミスキャス・モード

ifindex = socket.if_nametoindex(iface)
action = PACKET_MR_PROMISC
alen = 0
address = b'\0'

# バイト列オブジェクトを作成
packet_mreq = struct.pack('iHH8s', ifindex, type, alen, address)
```

C言語とPythonで互換性があるように、struct.packの第一引数で型の指定を行っています。意味は以下のようになります。

|フォーマット|Cの型|Pythonのタイプ|
|:--:|:--:|:--:|
|i|int|int|
|H|unsigned short int|int|
|8s|char[8]|bytes|

## ソケットオブジェクトの作成
Ethernetヘッダーからキャプチャするために、ソケットタイプにはSOCK_RAWを指定します。

```python

import socket

ETH_P_ALL = 3    # 全てのプロトコル
sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETH_P_ALL))
sock.bind((iface, 0))
```

## プロミスキャス・モードの有効化（ソケットオプションの設定）
プロミスキャス・モードを有効にするためにsetsockopt関数を使用してソケットオプションを設定します。
レベルには全プロトコルをキャプチャするためにSOL_PACKET(263)、オプション名にはPACKET_ADD_MEMBERSHIP(1)、オプション値には先に作成したバイト列オブジェクト`packet_mreq`を指定します。

```python

SOL_PACKET = 263
PACKET_ADD_MEMBERSHIP = 1
sock.setsockopt(SOL_PACKET, PACKET_ADD_MEMBERSHIP, packet_mreq)

while True:
    sock.recv(1024)    # パケット受信
sock.close()
```

これでプロミスキャス・モードが有効になりました。

# ソースコード
ここにはソースコードの中で、プロミスキャスモードの実装部分を載せておきます。
完全なソースコードは[こちら](https://github.com/homoluctus/pniffer)にあります。パケットのパーサーも開発途中ですが書いてあるので、本当にプロミスキャスモードが有効になっているのか検証しつつパケット解析してみてはいかがでしょうか？

```python:demo.py

import socket
import struct

interface = 'lo'

# For proto argument of socket object
ETH_P_ALL = 3   # All protocol pakcet

# For socket.setsockopt()
SOL_PACKET = 263    # Socket level
PACKET_ADD_MEMBERSHIP = 1   # Socket option name

# For mr_type (action) of packet_mreq structure 
PACKET_MR_PROMISC = 1   # Promiscuous mode

# For packet_mreq structure
mr_ifindex = socket.if_nametoindex(interface)   # c_type is int
mr_type = PACKET_MR_PROMISC                     # c_type is unsigned short
mr_alen = 0                                     # c_type is unsigned short
mr_address = b'\0'                              # c_type is unsigned char[8]

packet_mreq = struct.pack('iHH8s',
                          mr_ifindex,
                          mr_type,
                          mr_alen,
                          mr_address)

with socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETH_P_ALL)) as sock:
    sock.bind((interface, 0))
    sock.setsockopt(SOL_PACKET, PACKET_ADD_MEMBERSHIP, packet_mreq)

    try:
        while True:
            packet = sock.recv(1024)
            display_packet(packet)    # Function to display parsed packet
    except KeyboardInterrupt:
        print("EXIT")
    except:
        from traceback import print_exc
        print_exc()
```

プロミスキャスモードでパケットキャプチャするにはroot権限が必要です。
これを実行すると、kernelログに`device lo entered promiscuous mode`が出力されます。

Pythonでプロミスキャスモードを実装してみましたが、低レイヤーの処理をやるにはC/C++の方がいいですねw

# 参考
- [socket](https://docs.python.org/3.6/library/socket.html)
- [struct](https://docs.python.org/3.6/library/struct.html)
- [cpython/socketmodule.c at 3.6 · python/cpython](https://github.com/python/cpython/blob/3.6/Modules/socketmodule.c)