概要
VPNの実装で使われたりする仮想ネットワークドライバのTUN/TAPをPythonから操作してみる話。
https://github.com/kstm-su/dnsvpn を作ったときに調べた内容。
前提条件
- Mac or Linux (Windowsは未検証)
- Python (3.5で検証)
TUN/TAPについて
TAP は、イーサネットデバイスをシミュレートし、データリンク層の操作を行う。TUN はネットワーク層をシミュレートするもので、IPパケットなどを操作する。TAPはブリッジ生成に使われ、TUNはルーティングに使われる。
OSがTUN/TAPデバイスに送ったパケットは、そのデバイスに接続しているユーザープログラムに送信される。また、ユーザープログラムからTUN/TAPデバイスにパケットを送ることもできる。その場合、TUN/TAPデバイスはそれらのパケットをOSのプロトコルスタックに渡すので、OS側からはあたかも外部からパケットを受け取ったように見える。
L3以上でよければTUN、L2から扱いたいときはTAPを使えばよさそう。
TUN/TAPのインストール
Mac
brewでインストールできる。
$ brew cask install tuntap
/dev
以下に tun[0-15]
と tap[0-15]
があればOK。
Linux
カーネルが 2.2.x, 2.4.x 以降であれば標準で入っているはず
$ lsmod | grep tun
で何もでてこなければ modprobe
する
$ sudo modprobe tun
/dev/net/tun
があればOK。
TUN/TAPデバイスを登録する
ip link
とかでTUN/TAPが見えるようにします。
基本的にはデバイスファイルをopen
で開くだけですが、Linuxの場合はioctl
でデバイス名やフラグを設定する必要があって少々面倒です。
プログラムが終了するとデバイスの登録が解除されるのでサンプルプログラムでは無限ループで終了しないようにしています。
Mac
/dev/tun*
を開くだけ、簡単。
(TAPを使うときは /dev/tap*
)
# tuntap-darwin.py
import os
import time
tun = os.open('/dev/tun0', os.O_RDWR)
while True:
time.sleep(1)
実行してみるとデバイスファイル名をそのままネットワークデバイス名として認識しているのがわかります。
$ 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
にします。
# 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
に渡した文字列をネットワークデバイス名として認識しているのがわかります。
$ 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
すると怒られるので外部コマンドを実行してリンクアップしておきます。
# 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
を打ってみる
$ 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つ表示されています。
これは受信したパケットをそのまま送信しているため。