前回・前々回で、Python2 + Scapyで、Raspberry Pi 2 Model B をブリッジ化してみました。
「ルーター自作でわかるパケットの流れ」に従い、次はルータを作ります。
ただ、書籍ではL2ルータを実装していましたが、NAT/NAPTなルータが欲しかったので、L3ルータを作成してみました。
目次
環境
NAT/NAPTによるIPアドレス変換があるため、今回は
- 192.168.10.0/24
- 192.168.11.0/24
の環境を用意します。
各機器の役割は、ブリッジの時と同じように
- Mac = HTTPサーバ
- 無線LANアクセスポイント = スイッチ
- ラズパイ = NAT/NAPTルータ
- Windows10 = ルータにぶら下げたクライアント
とします。
ネットワーク構成
各機器の接続は、ブリッジの時と同じです。
-----------------------------------------
Mac
(`en0` : WiFi)
-----------------------------------------
|
|
-----------------------------------------
(無線)
無線LANアクセスポイント
(スイッチ)
-----------------------------------------
|
|
-----------------------------------------
(`eth0` : オンボードLANアダプタ)
Raspberry Pi 2 Model B
(`eth1` : 外付けUSB有線LANアダプタ)
-----------------------------------------
|
|
-----------------------------------------
(オンボードLANアダプタ)
Windows10
-----------------------------------------
明示的に設定したネットワークアダプタ設定です。
なお、以降の表記は
とします。
機器 |
アダプタ |
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 |
ラズパイ上の状態もこんな感じです。
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
- Raspberry Pi 2 Model B
- Raspbian
- RASPBIAN STRETCH WITH DESKTOP
- Version: November 2017
- Python 2.7.13
- Raspbianにインストールされていたものを使用
- Scapy 2.3.3
- 外付けUSB有線LANアダプタ(LUA4-U3-AGT)
- SSHでMacから接続可能
- Windows10
Scapyの関数を調べる
ルータ化するにあたり、Scapyで使えそうな関数を調べてみます。
Scapyを実行する機器のインタフェースからMACアドレス・IPアドレスを取得するには、
- MACアドレス取得:
scapy.all.get_if_hwaddr()
- IPアドレス取得:
scapy.all.get_if_addr()
を使います。
関数の引数には、インタフェース名(下記の例だと 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()
has_tcp = p.haslayer(IP)
tcp_layer = p.getlayer(IP)
なお、getlayer()では、該当レイヤよりも上のレイヤを取得します。
例えば p.getlayer(IP)
の場合、IPとその上のTCPを取得します。
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レイヤには chksum
属性があります。
アクセスするには、ドット(.
)と属性名を使います。
ip_packet.chksum
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
の場合、コンソールへ出力しません。
IP・TCP・UDPにはチェックサムがあります。
そのため、内容を変更した場合にはチェックサムを再計算する必要があります。
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()
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-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
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):
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 nat_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')
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
if is_ssh_packet(p):
continue
if p.dst == local_eth_mac_address:
packet_from_local_to_global = p.getlayer(IP)
nat_table[global_eth_ip_address] = packet_from_local_to_global.src
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(packet_to_global, verbose=0)
elif p.dst == global_eth_mac_address:
packet_from_global_to_local = p.getlayer(IP)
local_pc_ip_address = nat_table.get(packet_from_global_to_local.dst)
if local_pc_ip_address is None:
continue
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_table[global_eth_ip_address] = packet_from_local_to_global.src
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
packet_from_global_to_local.dst = local_pc_ip_address
という部分になります。このあと、IPパケットを再生成しています。
今回の通信の始まりはローカル側からの想定なため、上記のような簡易的なものにしてあります。
NATの動作確認
ラズパイをNATルータ化
作成したPythonスクリプトを実行し、ルータ化します。
pi@raspberrypi:~/router_jisaku $ sudo python scapy_nat_router_using_ip_layer.py
パケットのやり取りを確認するため、ラズパイの別のターミナルで 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
-e
port 8000
-nn
- ポート番号をプロトコル名に変換しない(ポート番号をそのまま表示する)
です。
Windowsで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
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_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)
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:
global_port = random.randint(49152, 65535)
napt_table[(global_eth_ip_address, global_port)] = \
(packet_from_local_to_global.src, packet_from_local_to_global.sport)
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)
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
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
参考資料
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