PythonでTUN/TAPを使う

# 概要

VPNの実装で使われたりする仮想ネットワークドライバのTUN/TAPをPythonから操作してみる話。
https://github.com/kstm-su/dnsvpn を作ったときに調べた内容。

## 前提条件

- Mac or Linux (Windowsは未検証)
- Python (3.5で検証)

## TUN/TAPについて

[Wikipedia](https://ja.wikipedia.org/wiki/TUN/TAP)より
>TAP は、イーサネットデバイスをシミュレートし、データリンク層の操作を行う。TUN はネットワーク層をシミュレートするもので、IPパケットなどを操作する。TAPはブリッジ生成に使われ、TUNはルーティングに使われる。

>OSがTUN/TAPデバイスに送ったパケットは、そのデバイスに接続しているユーザープログラムに送信される。また、ユーザープログラムからTUN/TAPデバイスにパケットを送ることもできる。その場合、TUN/TAPデバイスはそれらのパケットをOSのプロトコルスタックに渡すので、OS側からはあたかも外部からパケットを受け取ったように見える。

L3以上でよければTUN、L2から扱いたいときはTAPを使えばよさそう。

# TUN/TAPのインストール

## Mac
brewでインストールできる。

```bash
$ brew cask install tuntap
```

`/dev` 以下に `tun[0-15]` と `tap[0-15]` があればOK。

## Linux
カーネルが 2.2.x, 2.4.x 以降であれば標準で入っているはず

```bash
$ lsmod | grep tun
```
で何もでてこなければ `modprobe` する

```bash
$ sudo modprobe tun
```


`/dev/net/tun` があればOK。

# TUN/TAPデバイスを登録する
`ip link` とかでTUN/TAPが見えるようにします。
基本的にはデバイスファイルを`open`で開くだけですが、Linuxの場合は`ioctl`でデバイス名やフラグを設定する必要があって少々面倒です。
プログラムが終了するとデバイスの登録が解除されるのでサンプルプログラムでは無限ループで終了しないようにしています。

## Mac
`/dev/tun*` を開くだけ、簡単。
(TAPを使うときは `/dev/tap*`)

```py3
# tuntap-darwin.py
import os
import time
tun = os.open('/dev/tun0', os.O_RDWR)
while True:
    time.sleep(1)
```

実行してみるとデバイスファイル名をそのままネットワークデバイス名として認識しているのがわかります。

```bash
$ sudo python tuntap-darwin.py &
$ ip link show tun0
tun0: flags=8850<POINTOPOINT,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	open (pid 2251)
```

## Linux
`/dev/net/tun` を開いて `ioctl` で TUN or TAP とか デバイス名を設定します。
TAPを使うときは `TUN_TUN_DEV` => `TUN_TAP_DEV` にします。

```py3
# tuntap-linux.py
import os
import time
import fcntl

# https://github.com/kstm-su/dnsvpn/blob/master/lib/linux.py
import linux

tun = os.open('/dev/net/tun', os.O_RDWR)
ifr = linux.ifreq(name=b'hoge', flags=linux.IFF_NO_PI|linux.TUN_TUN_DEV)
fcntl.ioctl(tun, linux.TUNSETIFF, ifr)
while True:
    time.sleep(1)
```

実行してみると `ifreq` の引数 `name` に渡した文字列をネットワークデバイス名として認識しているのがわかります。

```bash
$ sudo python tuntap-linux.py &
$ ip addr show hoge
6: hoge: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc noop state DOWN qlen 500
    link/none
```

# TUN/TAPで通信する
登録したTUN/TAPデバイスを使ってデータを送受信してみます。
MacでもLinuxでも `read` で受信、 `write` で送信するだけです。
ただ、このまま`read`とか`write`すると怒られるので外部コマンドを実行してリンクアップしておきます。

```py3
# tuntap.py
import os
import subprocess
tun = os.open('/dev/tun0', os.O_RDWR)
subprocess.check_call('sudo ifconfig tun0 192.168.100.2 192.168.100.1 netmask 255.255.255.0 up', shell=True)
while True:
    data = os.read(tun, 1500)
    print(data)
    os.write(tun, data)
```

作ったネットワークデバイスに `ping` を打ってみる

```bash
$ sudo python3 tuntap.py &
$ ping 192.168.100.1
PING 192.168.100.1 (192.168.100.1): 56 data bytes
b'E\x00\x00Tj\x15\x00\x00@\x01\xc7?\xc0\xa8d\x02\xc0\xa8d\x01\x08\x00t\xa4\x7f\x0e\x00\x00XA\xcbh\x00\x07\xf5\x98\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567'
Request timeout for icmp_seq 0
b'E\x00\x00T~\xa2\x00\x00@\x01\xb2\xb2\xc0\xa8d\x02\xc0\xa8d\x01\x08\x00dk\x7f\x0e\x00\x01XA\xcbi\x00\x08\x05\xcf\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567'
Request timeout for icmp_seq 1
b'E\x00\x00T\xa3\x8e\x00\x00@\x01\x8d\xc6\xc0\xa8d\x02\xc0\xa8d\x01\x08\x00b7\x7f\x0e\x00\x02XA\xcbj\x00\x08\x08\x01\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567'
Request timeout for icmp_seq 2
b'E\x00\x00T2\x86\x00\x00@\x01\xfe\xce\xc0\xa8d\x02\xc0\xa8d\x01\x08\x00P\xf3\x7f\x0e\x00\x03XA\xcbk\x00\x08\x19C\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567'
```

Wiresharkで見ると1回のpingにつき2つ表示されています。
これは受信したパケットをそのまま送信しているため。
![スクリーンショット 2016-12-03 4.32.29.png](https://qiita-image-store.s3.amazonaws.com/0/69029/f62720c7-b1a8-34ea-1f9f-cb42e528dd23.png)