• 6
    いいね
  • 0
    コメント

概要

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

前提条件

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

TUN/TAPについて

Wikipediaより

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つ表示されています。
これは受信したパケットをそのまま送信しているため。
スクリーンショット 2016-12-03 4.32.29.png

この投稿は 信州大学 kstm Advent Calendar 20164日目の記事です。