2012年08月14日

Python on Ubuntuで簡易NATルータを実装する方法

先日、Ubuntuで自作IPhoneテザリング環境を構築するという話をしましてPythonパケット操作ライブラリ(Scapy)を触ってたんですが、このScapyの面白さに軽くハマりましてPython+Scapy+Ubuntuで簡易NATルータを作ってみることにしました。

09

UbuntuノートPCのWiFiモジュールをWiFiアクセス・ポイント化し、クライアント端末のトラフィックをもう一つのインタフェース(eth0)を使ってインターネットに接続させます。

では早速。

Python・Scapy環境の準備


まず、Python環境を構築します。Pythonをインストール後、次の手順でScapyをインストールします。

$ sudo apt-get install wget
$ wget http://www.secdev.org/projects/scapy/files/scapy-latest.tar.gz
$ tar xzf scapy-latest.tar.gz 
$ cd scapy-2.1.0/
$ sudo python setup.py install 

後もう一つ、DNSパケットのエンコード、デコードが失敗してしまうので、scapyにパッチを当てましょう。

こちらからdns.patchファイルをダウンロードして、次のようにpatchコマンドを実行。

$ cd /usr/local/lib/python2.7/dist-packages/scapy/layers/
 (環境によってpythonのバージョン、パスは異なります)
$ sudo patch < /path/to/dns.patch 

これでPython、Scapyの環境構築はおしまい。


Pythonで簡易NATルータを実装する


実際に作ってみたのがこちらのコード(Python素人なので細かいところはご容赦下さい・・・)。

import os,sys,getopt,struct,re,string,logging
from socket import*
from fcntl  import ioctl
from select import select 
from scapy.all import*
from datetime import datetime
from collections import defaultdict
import threading

OUT_ETH_IFNAME = "eth0"
outHwaddr = get_if_hwaddr(OUT_ETH_IFNAME)
outIpaddr = get_if_addr(OUT_ETH_IFNAME)

IN_ETH_IFNAME = "wlan0"
inHwaddr = get_if_hwaddr(IN_ETH_IFNAME)
inIpaddr = get_if_addr(IN_ETH_IFNAME)

outPackets = defaultdict(int)
outPacketToLanAddr = defaultdict(str)

class OutPacketHandler(threading.Thread):

  def __init__(self):
    threading.Thread.__init__(self)
    
  def run(self):
    print "Starting outboud..."
    l2s = conf.L2listen(iface = OUT_ETH_IFNAME)
    global outHwaddr
    global outIpaddr
    global outPackets
    global outPacketToLanAddr

    while (True):
      eframe = l2s.recv(1522)
      #print self.ifname+":"+eframe.summary()
      if eframe.dst != outHwaddr or not eframe[0].haslayer(IP):
        continue

      ipPkt = eframe[0][IP]

      if outPackets[(ipPkt.src, ipPkt.sport)] > 0:
        outPackets[(ipPkt.src, ipPkt.sport)] -= 1
        ipPkt.dst = outPacketToLanAddr[(ipPkt.src, ipPkt.sport)]
        if ipPkt.haslayer(TCP): del ipPkt[TCP].chksum
        if ipPkt.haslayer(UDP): del ipPkt[UDP].chksum
        del ipPkt[IP].chksum
        ipPkt = IP(str(ipPkt)) # recompute chksum
        print "OUT-TO-IN PACKET: "+ipPkt.summary()
        send(ipPkt, verbose=0)
        

class InPacketHandler(threading.Thread):

  def __init__(self):
    threading.Thread.__init__(self)
    
  def run(self):
    print "Starting intboud..."
    l2s = conf.L2listen(iface = IN_ETH_IFNAME)
    global inHwaddr
    global inIpaddr
    global outIpaddr
    global outPackets
    global outPacketToLanAddr

    while (True):
      eframe = l2s.recv(1522)

      if eframe.dst != inHwaddr or not eframe[0].haslayer(IP):
        continue

      ipPkt = eframe[0][IP]

      if ipPkt.dst != inIpaddr:
        outPackets[(ipPkt.dst, ipPkt.dport)] += 1
        outPacketToLanAddr[(ipPkt.dst, ipPkt.dport)] = ipPkt.src
        ipPkt.src = outIpaddr
        if ipPkt.haslayer(TCP): del ipPkt[TCP].chksum
        if ipPkt.haslayer(UDP): del ipPkt[UDP].chksum
        del ipPkt[IP].chksum
        ipPkt = IP(str(ipPkt)) # recompute chksum
        print "IN-TO-OUT PACKET: "+ipPkt.summary()
        send(ipPkt, verbose=0)

outside = OutPacketHandler()
outside.daemon = True
outside.start()

inside = InPacketHandler()
inside.daemon = True
inside.start()

os.system("sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP")

input("Press Ctrl+C to stop.")
   ※ダウンロード

OUT(インターネット)側をeth0、IN(LAN)側をwlan0にしています。

OUT_ETH_IFNAME = "eth0"
outHwaddr = get_if_hwaddr(OUT_ETH_IFNAME)
outIpaddr = get_if_addr(OUT_ETH_IFNAME)

IN_ETH_IFNAME = "wlan0"
inHwaddr = get_if_hwaddr(IN_ETH_IFNAME)
inIpaddr = get_if_addr(IN_ETH_IFNAME)

僕は無線LANモジュールをアクセス・ポイント化し、LAN側インタフェースに利用しましたが、優先ポートが複数ある場合はethポートを使っても良いですね。

UbuntuでのWiFiアクセス・ポイント構築方法はこちらの記事をどうぞ(PythonでNATルータを実装するのでiptablesでMASQUERADEはしないこと)。

Ubuntuで無線LANルーターを構築する

さあ、先に進みましょう。netb-nat.pyではOutPacketHandler、InPacketHandlerの2つのクラスを用意しています。どちらも基本は同じ。conf.L2listen()でレイヤー2(Ethernet)のフレームをスニッフィングし、最長データ長である1522バイト毎に読み込んでいます。

l2s = conf.L2listen(iface = OUT_ETH_IFNAME)
・・・
eframe = l2s.recv(1522)

InPacketHandlerクラスでは、受信したEthernetフレームの内、IPレイヤーが含まれていない、宛先が自身のMACアドレス(inHwAddr)では無い場合は何もせず、次の受信を待ちます。

      if eframe.dst != inHwaddr or not eframe[0].haslayer(IP):
        continue

この条件に該当しない場合にはIPパケットの宛先アドレスを参照し、自身のIPアドレスでは無い場合、NAT処理に入ります。
(Ethernetフレームの宛先MACアドレスは自分自身だが、IPパケットの宛先アドレスは自分では無い、つまりゲートウェイ向けのパケットということですね)

      if ipPkt.dst != inIpaddr:
        outPackets[(ipPkt.dst, ipPkt.dport)] += 1
        outPacketToLanAddr[(ipPkt.dst, ipPkt.dport)] = ipPkt.src
        ipPkt.src = outIpaddr
 
NAT処理では、送信元IPアドレスを自身のOUT側IPアドレスに変換しますが、後で元に戻せるよう送信先IPアドレスとポート番号をキーにしてパケット送信回数(outPackets)、送信元IPアドレス(outPacketToLanAddr)は覚えておきます(厳密には矛盾がありますが今回は簡易実装)。 
 
最後にこのパケットのチェックサムを再計算し、send()メソッドで非同期(受信を待たずに)に送信します。

        if ipPkt.haslayer(TCP): del ipPkt[TCP].chksum
        if ipPkt.haslayer(UDP): del ipPkt[UDP].chksum
        del ipPkt[IP].chksum
        ipPkt = IP(str(ipPkt)) # recompute chksum
        print "IN-TO-OUT PACKET: "+ipPkt.summary()
        send(ipPkt, verbose=0)

ね、簡単でしょ?

OutPacketHandlerクラスも同じくOUT側であるeth0をスニッフィングし、受信したパケットの送信元アドレス、ポート番号から、NATパケットか否かを判断し、該当する場合には宛先アドレスをLAN側クライアントのIPアドレスに変換してLAN側に再送信します。

      if outPackets[(ipPkt.src, ipPkt.sport)] > 0:
        outPackets[(ipPkt.src, ipPkt.sport)] -= 1
        ipPkt.dst = outPacketToLanAddr[(ipPkt.src, ipPkt.sport)]
        if ipPkt.haslayer(TCP): del ipPkt[TCP].chksum
        if ipPkt.haslayer(UDP): del ipPkt[UDP].chksum
        del ipPkt[IP].chksum
        ipPkt = IP(str(ipPkt)) # recompute chksum
        print "OUT-TO-IN PACKET: "+ipPkt.summary()
        send(ipPkt, verbose=0)

最後にこの2つのクラスをスレッドとして起動し、iptablesでRSTフラグの有効になったTCPパケットが勝手に送信されないようにしています。

outside = OutPacketHandler()
outside.daemon = True
outside.start()

inside = InPacketHandler()
inside.daemon = True
inside.start()

os.system("sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP")

input("Press Ctrl+C to stop.")


その速度は?


次のコマンドでNATルータを起動しましょう。

$ sudo python netb-nat.py

NAT処理が始まると、次のようなメッセージが出力されます。
(ここでは192.168.0.17がUbuntuのOUT側IPアドレス、10.0.0.100がLAN側クライアント)

IN-TO-OUT PACKET: IP/TCP 192.168.0.17:62309 > 27.34.160.198:www A/Raw
OUT-TO-IN PACKET: IP/TCP 27.34.160.198:www > 10.0.0.100:62309 A
IN-TO-OUT PACKET: IP/TCP 192.168.0.17:62309 > 27.34.160.198:www A/Raw
OUT-TO-IN PACKET: IP/TCP 27.34.160.198:www > 10.0.0.100:62309 A
IN-TO-OUT PACKET: IP/TCP 192.168.0.17:62309 > 27.34.160.198:www A/Raw
OUT-TO-IN PACKET: IP/TCP 27.34.160.198:www > 10.0.0.100:62309 A

気になる速度は、僕のUbuntuが非力(Atom)なので滅茶苦茶遅いですね。

49

しかし、故意に遅延、パケット・ロスを起こすことも出来るので使い方次第では面白いかもしれませんね。



Linuxネットワークプログラミングバイブル
小俣 光之 種田 元樹 
秀和システム 
売り上げランキング: 190195

ルーター自作でわかるパケットの流れ
小俣 光之 
技術評論社 
売り上げランキング: 46126

Posted by netbuffalo at 15:31│Comments(0)TrackBack(0)ネットワーク 


この記事へのトラックバックURL

http://trackback.blogsys.jp/livedoor/netbuffalo/4140240

コメントする

名前
URL
 
  絵文字