前回・前々回で、Python2 + Scapyで、Raspberry Pi 2 Model B をブリッジ化してみました。
- Python2 + Scapyで、Raspberry Pi 2 Model B をブリッジにできるか試してみた #router_jisaku - メモ的な思考的な
- Scapy + Python2 + I/O多重化・ブロッキングI/O系モジュールにて、Raspberry Pi 2 Model Bをブリッジ化する - メモ的な思考的な
「ルーター自作でわかるパケットの流れ」に従い、次はルータを作ります。
- 作者: 小俣 光之
- 出版社/メーカー: 技術評論社
- 発売日: 2011/07/09
- メディア: 単行本(ソフトカバー)
- 購入: 4人 クリック: 130回
- この商品を含むブログ (12件) を見る
ただ、書籍ではL2ルータを実装していましたが、NAT/NAPTなルータが欲しかったので、L3ルータを作成してみました。
目次
- 環境
- Scapyの関数を調べる
- RSTフラグの立ったパケットをルータから送信しない方法の調査
- NATルータの実装
- NATの動作確認
- NAPTルータの実装
- NAPTルータの動作確認
- 参考資料
- ソースコード
環境
NAT/NAPTによるIPアドレス変換があるため、今回は
- 192.168.10.0/24
- 192.168.11.0/24
の環境を用意します。
各機器の役割は、ブリッジの時と同じように
とします。
ネットワーク構成
各機器の接続は、ブリッジの時と同じです。
----------------------------------------- Mac (`en0` : WiFi) ----------------------------------------- | | ----------------------------------------- (無線) 無線LANアクセスポイント (スイッチ) ----------------------------------------- | | ----------------------------------------- (`eth0` : オンボードLANアダプタ) Raspberry Pi 2 Model B (`eth1` : 外付けUSB有線LANアダプタ) ----------------------------------------- | | ----------------------------------------- (オンボードLANアダプタ) Windows10 -----------------------------------------
ネットワークアダプタ設定
明示的に設定したネットワークアダプタ設定です。
なお、以降の表記は
- 無線LANアクセスポイント = 無線LAN AP
- Raspberry Pi 2 Model B = ラズパイ
- DGW = デフォルトゲートウェイ
とします。
機器 | アダプタ | IPアドレス | DGW | DNS |
---|---|---|---|---|
Mac | en0 | 192.168.10.101/24 | 192.168.10.1 | 192.168.10.1 |
無線LAN AP | 192.168.10.1/24 | |||
ラズパイ | eth0 | 192.168.10.50/24 | 192.168.10.1 | 192.168.10.1 |
ラズパイ | eth1 | 192.168.11.60/24 | ||
Windows | 192.168.11.201/24 | 192.168.11.60 | 192.168.10.1 |
ラズパイ上の状態もこんな感じです。
# インタフェースのIPアドレス pi@raspberrypi:~ $ ifconfig eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 192.168.10.50 netmask 255.255.255.0 broadcast 192.168.10.255 eth1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 192.168.11.60 netmask 255.255.255.0 broadcast 192.168.11.255 # ルーティング情報 pi@raspberrypi:~ $ route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 192.168.10.1 0.0.0.0 UG 202 0 0 eth0 192.168.10.0 0.0.0.0 255.255.255.0 U 202 0 0 eth0 192.168.11.0 0.0.0.0 255.255.255.0 U 203 0 0 eth1
各機器の構成
こちらもブリッジの時と同じです。
- Mac OS X 10.11.6
- 疎通確認のために、PythonでHTTPサーバを立てる
- Raspberry Pi 2 Model B
- Windows10
- curlをインストール済
Scapyの関数を調べる
ルータ化するにあたり、Scapyで使えそうな関数を調べてみます。
インタフェースのMACアドレス・IPアドレス取得
Scapyを実行する機器のインタフェースからMACアドレス・IPアドレスを取得するには、
を使います。
関数の引数には、インタフェース名(下記の例だと eth0
)を渡します。
from scapy.all import get_if_hwaddr, get_if_addr global_eth_mac_address = get_if_hwaddr('eth0') global_eth_ip_address = get_if_addr('eth0')
パケット中のレイヤの存在確認とレイヤ取得
パケットの各レイヤの存在確認と取得は、パケットの
- レイヤの存在確認:
haslayer()
- レイヤ取得:
getlayer()
を使います。
from scapy.all import conf from scapy.layers.inet import TCP s = conf.L2socket(iface='eth0') p = s.recv() # IPレイヤ存在確認 has_tcp = p.haslayer(IP) # IPレイヤ取得 tcp_layer = p.getlayer(IP)
なお、getlayer()では、該当レイヤよりも上のレイヤを取得します。
例えば p.getlayer(IP)
の場合、IPとその上のTCPを取得します。
# -*- coding: utf-8 -*- from select import select from scapy.all import conf from scapy.layers.inet import IP while True: l2s = conf.L2socket(iface='eth0') r, _, _ = select([l2s], [], []) for s in r: p = s.recv() if p and p.haslayer(IP): layer = p.getlayer(IP) layer.show() # => show()の結果 # ###[ IP ]### # version = 4L # ihl = 5L # tos = 0x0 # len = 64 # id = 37143 # flags = DF # frag = 0L # ttl = 64 # proto = tcp # chksum = 0x13b9 # src = 192.168.10.101 # dst = 192.168.10.50 # \options \ # ###[ TCP ]### # sport = 56898 # dport = http # seq = 1880557107 # ack = 0 # dataofs = 11L # reserved = 0L # flags = S # window = 65535 # chksum = 0xa411 # urgptr = 0 # options = [('MSS', 1460), ('NOP', None), ('WScale', 5), ('NOP', None), ('NOP', None), ('Timestamp', (591430886, 0)), ('SAckOK', ''), ('EOL', None)]
レイヤ属性へのアクセス
上のshow()の結果を見ると、IPレイヤには chksum
属性があります。
アクセスするには、ドット(.
)と属性名を使います。
# IPレイヤのチェックサム(chksum) へアクセス ip_packet.chksum # TCPレイヤの宛先ポートへのアクセス tcp_layer = target_packet.getlayer(TCP) dport = tcp_layer.dport
パケットの送信
送信方法は色々とありますが、今回は send()
関数を使います。
send関数は、レイヤ3以上(IPレイヤ)以上のパケットを送信します。その際のレイヤ2はScapyがいい感じに作成します。
send(packet_to_global, verbose=0)
なお、引数 verbose
は、送信ログをコンソール出力するかを決めます。
デフォルト値は 2
のようで
.
Sent 1 packets.
が出力されます。
verbose=0
の場合、コンソールへ出力しません。
チェックサムの再計算
そのため、内容を変更した場合にはチェックサムを再計算する必要があります。
Scapyでの再計算を調べてみると、del文でチェックサムを削除した後でパケットを再生成すれば良いようです。
python - How to calculate a packet checksum without sending it? - Stack Overflow
del ip_packet.chksum result = IP(str(ip_packet))
パケットの情報表示
show()
summary()
mysummary()
があります。
それぞれの違いは以下です。
layer = p.getlayer(IP) print '---- show() ----' layer.show() print '---- summary() ----' print layer.summary() print '---- mysummary() ----' print layer.mysummary() # => 実行結果 # ---- show() ---- # ###[ IP ]### # version = 4L # ihl = 5L # tos = 0x0 # len = 64 # id = 19636 # flags = DF # frag = 0L # ttl = 64 # proto = tcp # chksum = 0x581c # src = 192.168.10.101 # dst = 192.168.10.50 # \options \ # ###[ TCP ]### # sport = 56968 # dport = http # seq = 2173034460L # ack = 0 # dataofs = 11L # reserved = 0L # flags = S # window = 65535 # chksum = 0x8485 # urgptr = 0 # options = [('MSS', 1460), ('NOP', None), ('WScale', 5), ('NOP', None), ('NOP', None), ('Timestamp', (592296199, 0)), ('SAckOK', ''), ('EOL', None)] # # ---- summary() ---- # IP / TCP 192.168.10.101:56968 > 192.168.10.50:http S # ---- mysummary() ---- # 192.168.10.101 > 192.168.10.50 tcp
RSTフラグの立ったパケットをルータから送信しない方法の調査
今回は厳密なルータを作成するわけではないため、NAT/NAPTした時に発生するRST(Reset)フラグの立ったパケットについては、ルータから送信しないようにします。
これを送信してしまうと、TCPのやり取りが途中で中断されてしまうため、NAT/NAPTがうまく動かないです。
NAT の理解は芋づる式 - インフラまわりのプロになりたい
そのため、
os.system('sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP')
と、iptablesのルールを追加します。
なお、iptablesのオプションの意味は、
-A OUTPUT
- OUTPUTチェイン(ローカルで生成されたパケットをルーティングの前に変換するためのチェイン)に、規則を追加する
-p tcp
- プロトコル
TCP
に対する処理
- プロトコル
--tcp-flags RST RST
- RSTフラグがセットされているTCPパケットに対する処理
-j DROP
- ここまでの規則にマッチしたパケットを破棄する
です。
参考:
NATルータの実装
ここまでで調査が終わったため、次はルータを実装していきます。
なお、前回、I/O処理をいろいろと試しましたが、今回はScapyのsniff()関数に合わせて、I/O多重化の select()
だけの実装例となります。
また、MacからSSHでラズパイに接続していることもあり、SSHのパケットが入ってきます。動作が遅くなったりするので、SSHパケットの場合は処理しないようにしています。本来は不要な処理ですが...
scapy_nat_router_using_ip_layer.py
# -*- coding: utf-8 -*- from scapy.all import conf, send, get_if_hwaddr, get_if_addr from select import select from scapy.layers.inet import IP, TCP, UDP import os def recreate_ip_packet(ip_packet): # 各レイヤのチェックサムは、削除することでScapyが再計算してくれる # https://stackoverflow.com/questions/5953371/how-to-calculate-a-packet-checksum-without-sending-it if ip_packet.haslayer(TCP): del ip_packet.getlayer(TCP).chksum if ip_packet.haslayer(UDP): del ip_packet.getlayer(UDP).chksum del ip_packet.chksum # 再計算するために、IPレイヤより上層のパケットインスタンスを生成する result = IP(str(ip_packet)) return result def is_ssh_packet(target_packet): if not target_packet.haslayer(TCP): return False tcp_layer = target_packet.getlayer(TCP) return tcp_layer.dport == 22 def nat_router(): try: global_socket = conf.L2socket(iface='eth0') local_socket = conf.L2socket(iface='eth1') # ルータのインタフェースのMACアドレス・IPアドレスを取得しておく global_eth_mac_address = get_if_hwaddr('eth0') global_eth_ip_address = get_if_addr('eth0') local_eth_mac_address = get_if_hwaddr('eth1') # RSTパケットをラズパイから送信すると、TCPが途中で切れてしまうので、送信しないようにする os.system('sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP') # NATテーブル nat_table = {} while True: readable_sockets, _, _ = select([global_socket, local_socket], [], []) for s in readable_sockets: p = s.recv() if not p or not p.haslayer(IP): continue # 今回の場合、MacとラズパイをSSHでつないでいることからSSHパケットは除外する # 除外しないとパケット量が増えてしまい、curlがタイムアウトしてしまう if is_ssh_packet(p): continue # ローカルからグローバルへ抜けるパケットが入ってきた場合 if p.dst == local_eth_mac_address: # IPレイヤのパケットを取得する packet_from_local_to_global = p.getlayer(IP) # レスポンスパケットのIPアドレスを変換するため、 # 変換後のIPアドレスをキーとして、変換前のIPアドレスを保管しておく nat_table[global_eth_ip_address] = packet_from_local_to_global.src # 送信元IPアドレスを、ルーターのグローバル側IPアドレスに差し替える packet_from_local_to_global.src = global_eth_ip_address # 送信用パケットを再生成する packet_to_global = recreate_ip_packet(packet_from_local_to_global) # 送信用パケットの概要をコンソールに表示する print p.summary() # send()はLayer3のパケットをいい感じに送信してくれる # verbose=2(デフォルト値)など、1~3の場合、以下が表示される # . # Sent 1 packets. # verbose=0の場合、何も出力されない send(packet_to_global, verbose=0) # グローバルからローカルへ抜けるパケットが入ってきた場合 elif p.dst == global_eth_mac_address: packet_from_global_to_local = p.getlayer(IP) # 変換後のIPアドレスから変換前IPアドレスを取得する local_pc_ip_address = nat_table.get(packet_from_global_to_local.dst) # 念のため、変換できない場合はパケットを送信しない if local_pc_ip_address is None: continue # 宛先IPアドレスを変換前IPアドレスに差し替える packet_from_global_to_local.dst = local_pc_ip_address packet_to_local = recreate_ip_packet(packet_from_global_to_local) print p.summary() send(packet_to_local, verbose=0) except KeyboardInterrupt: pass if __name__ == '__main__': nat_router()
NAT処理は
# ローカルからグローバルへの送信 ## NATテーブルへローカルIPアドレスを保持 nat_table[global_eth_ip_address] = packet_from_local_to_global.src ## 送信元IPアドレスを、ルーターのグローバル側IPアドレスに差し替える packet_from_local_to_global.src = global_eth_ip_address # グローバルからローカルへの送信 local_pc_ip_address = nat_table.get(packet_from_global_to_local.dst) ## 念のため、変換できない場合はパケットを送信しない if local_pc_ip_address is None: continue ## 宛先IPアドレスを変換前IPアドレスに差し替える packet_from_global_to_local.dst = local_pc_ip_address
という部分になります。このあと、IPパケットを再生成しています。
今回の通信の始まりはローカル側からの想定なため、上記のような簡易的なものにしてあります。
NATの動作確認
ラズパイをNATルータ化
pi@raspberrypi:~/router_jisaku $ sudo python scapy_nat_router_using_ip_layer.py
tcpdumpでのパケット確認
パケットのやり取りを確認するため、ラズパイの別のターミナルで tcpdump
を実行しておきます。
pi@raspberrypi:~ $ sudo tcpdump -i eth0 -e port 8000 -nn tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
なお、tcpdumpのオプションの意味は、
-i eth0
- インタフェースeth0に対し、パケット確認する
-e
- リンクレベルヘッダ(MAC)もダンプに表示する
port 8000
- ポート8000番のパケットを確認する
-nn
- ポート番号をプロトコル名に変換しない(ポート番号をそのまま表示する)
です。
Windowsからcurlで動作確認
# curlの実行 >curl http://192.168.10.101:8000 # 実行結果 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> ... </html>
とHTTPレスポンスがありました。
HTTPサーバのログを見ると、アクセスがあったようです。
192.168.10.50 - - [06/Jan/2018 23:41:47] "GET / HTTP/1.1" 200 -
ラズパイのルータログにも出力がありました。
Ether / IP / TCP 192.168.10.50:64314 > 192.168.10.101:8000 S / Padding Ether / IP / TCP 192.168.10.101:8000 > 192.168.11.201:64314 SA Ether / IP / TCP 192.168.10.50:64314 > 192.168.10.101:8000 A / Padding ...
また、tcpdumpにも出力されていました。
14:41:46.288642 b8:xx:xx:xx:xx:xx > c4:xx:xx:xx:xx:xx, ethertype IPv4 (0x0800), length 68: 192.168.10.50.64314 > 192.168.10.101.8000: Flags [S], seq 3620418925, win 64240, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0 14:41:46.357560 c4:xx:xx:xx:xx:xx > b8:xx:xx:xx:xx:xx, ethertype IPv4 (0x0800), length 66: 192.168.10.101.8000 > 192.168.10.50.64314: Flags [S.], seq 4046793993, ack 3620418926, win 65535, options [mss 1460,nop,wscale 5,sackOK,eol], length 0 14:41:46.907171 b8:xx:xx:xx:xx:xx > c4:xx:xx:xx:xx:xx, ethertype IPv4 (0x0800), length 62: 192.168.10.50.64314 > 192.168.10.101.8000: Flags [.], ack 1, win 256, length 0 ...
これらより、NATルータ化が成功したと考えられました。
NAPTルータの実装
続いて、NAPTルータの実装を行います。
内容はほぼ同じですが、ローカルIPアドレスとポート番号をNAPTテーブルに保存しておくところが異なります。
なお、かなり遅い実装なため、curlがタイムアウトする恐れがあります...
scapy_napt_router_using_ip_layer.py
# -*- coding: utf-8 -*- from scapy.all import conf, send, get_if_hwaddr, get_if_addr from select import select from scapy.layers.inet import IP, TCP, UDP import os import random def recreate_ip_packet(ip_packet): if ip_packet.haslayer(TCP): del ip_packet.getlayer(TCP).chksum if ip_packet.haslayer(UDP): del ip_packet.getlayer(UDP).chksum del ip_packet.chksum result = IP(str(ip_packet)) return result def is_ssh_packet(target_packet): if not target_packet.haslayer(TCP): return False tcp_layer = target_packet.getlayer(TCP) return tcp_layer.dport == 22 def napt_router(): try: global_socket = conf.L2socket(iface='eth0') local_socket = conf.L2socket(iface='eth1') global_eth_mac_address = get_if_hwaddr('eth0') global_eth_ip_address = get_if_addr('eth0') local_eth_mac_address = get_if_hwaddr('eth1') os.system('sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP') # NAPTテーブル napt_table = {} while True: readable_sockets, _, _ = select([global_socket, local_socket], [], []) for s in readable_sockets: p = s.recv() if not p or not p.haslayer(IP): continue if is_ssh_packet(p): continue if p.dst == local_eth_mac_address: packet_from_local_to_global = p.getlayer(IP) # すでにNAPTで使用したローカルIP&ポートの場合は、以前のグローバルIP&ポートを使う # IPとポートが変わってしまうと、TCPコネクションがうまくいかず、通信が途切れる if (packet_from_local_to_global.src, packet_from_local_to_global.sport) \ in napt_table.values(): for global_eth_ip_address, global_port in napt_table.keys(): if napt_table[(global_eth_ip_address, global_port)] == \ (packet_from_local_to_global.src, packet_from_local_to_global.sport): break else: # まだNAPTで使用していないローカルIP&ポートの場合 # NAPTで使う変換後のポートをランダムに取得する # ポートの範囲は、プライベートポート番号とする global_port = random.randint(49152, 65535) # レスポンスパケットのIPアドレス・ポートを変換するため、 # 変換後のIPアドレス・ポートをキーとして、変換前のIPアドレス・ポートを保管しておく napt_table[(global_eth_ip_address, global_port)] = \ (packet_from_local_to_global.src, packet_from_local_to_global.sport) # 送信元IPアドレスとポートを、ルーターのグローバル側IPアドレスとランダムなポートに差し替える packet_from_local_to_global.src = global_eth_ip_address packet_from_local_to_global.sport = global_port packet_to_global = recreate_ip_packet(packet_from_local_to_global) print packet_to_global.summary() send(packet_to_global, verbose=0) elif p.dst == global_eth_mac_address: packet_from_global_to_local = p.getlayer(IP) # 変換後のIPアドレス・ポートから変換前IPアドレスを取得する local_pc_ip_address, local_pc_port = napt_table.get( (packet_from_global_to_local.dst, packet_from_global_to_local.dport)) # 念のため、変換できない場合はパケットを送信しない if local_pc_ip_address is None or local_pc_port is None: continue # 宛先IPアドレス・ポートを変換前IPアドレス・ポートに差し替える packet_from_global_to_local.dst = local_pc_ip_address packet_from_global_to_local.dport = local_pc_port packet_to_local = recreate_ip_packet(packet_from_global_to_local) print packet_to_local.summary() send(packet_to_local, verbose=0) except KeyboardInterrupt: pass if __name__ == '__main__': napt_router()
NAPTルータの動作確認
NATの時と同じように
したところ、うまく動作しました。
HTTPレスポンス
>curl http://192.168.10.101:8000 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> ... </html>
HTTPサーバのログにも
192.168.10.50 - - [06/Jan/2018 23:49:06] "GET / HTTP/1.1" 200 -
NAPTルータのログ
pi@raspberrypi:~/router_jisaku $ sudo python scapy_napt_router_using_ip_layer.py IP / TCP 192.168.10.50:57112 > 192.168.10.101:8000 S / Padding IP / TCP 192.168.10.101:8000 > 192.168.11.201:64318 SA IP / TCP 192.168.10.50:57112 > 192.168.10.101:8000 A / Padding ...
tcpdumpのログ
14:49:05.588728 b8:xx:xx:xx:xx:xx > c4:xx:xx:xx:xx:xx, ethertype IPv4 (0x0800), length 68: 192.168.10.50.57112 > 192.168.10.101.8000: Flags [S], seq 2576276753, win 64240, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0 14:49:05.590540 c4:xx:xx:xx:xx:xx > b8:xx:xx:xx:xx:xx, ethertype IPv4 (0x0800), length 66: 192.168.10.101.8000 > 192.168.10.50.57112: Flags [S.], seq 1977036037, ack 2576276754, win 65535, options [mss 1460,nop,wscale 5,sackOK,eol], length 0 14:49:06.131767 b8:xx:xx:xx:xx:xx > c4:xx:xx:xx:xx:xx, ethertype IPv4 (0x0800), length 62: 192.168.10.50.57112 > 192.168.10.101.8000: Flags [.], ack 1, win 256, length 0
参考資料
- Python on Ubuntuで簡易NATルータを実装する方法
- https://github.com/obonaventure/cnp3/blob/master/Missions/S8/Nat.py
ソースコード
GitHubに上げました。ディレクトリ scapy_python2/router/
にある
- scapy_nat_router_using_ip_layer.py
- scapy_napt_router_using_ip_layer.py
が今回のファイルです。
https://github.com/thinkAmi-sandbox/syakyo-router_jisaku