ろうとるがPythonを扱う、、（その21：ScapyでPort Forward）

# ScapyでPort Forwardを実現
今回のPythonもScapyのネタ。Port Forwardを実現してみる。

## ネットワーク
今回も、VirtualBoxのVM利用。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/6d59c5f2-1529-6829-52d9-397c98b78eee.png)
NAT環境下とみなし、Port Forwardを行うNATルーターが中央に位置する。左側がNATの外側、右側がNATの内側。左側のPCからNATルーターのWAN側IPアドレス（10.1.1.254）の特定TCPポート（ここでは、1111と2222）にアクセスすると、設定したNAT内側PCに転送する（1111：192.168.10.1、2222：192.168.10.2）ものである。

## ソースコード
参考情報は下記であり、多くの部分を利用させて頂いた。
- [Python2 + Scapyで、Raspberry Pi 2 Model B をNAT/NAPTルータ化してみた #router_jisaku](https://thinkami.hatenablog.com/entry/2018/01/07/121824)

### import
```python
from scapy.all import *
import select
import os
```
ここは特記事項なし。

### IPパケット再作成
```python
# Recreate IP Packet #
def recreate_ip_packet(ip_pkt):
    # Removal Check-Sum
    if ip_pkt.haslayer(TCP):
        del ip_pkt.getlayer(TCP).chksum
    if ip_pkt.haslayer(UDP):
        del ip_pkt.getlayer(UDP).chksum
    if ip_pkt.haslayer(ICMP):
        del ip_pkt.getlayer(ICMP).chksum
    del ip_pkt.chksum

    result = IP(ip_pkt.build())
    return result
```
IPアドレス変更などによるIPパケット再作成。ここは、以前の記事「[ろうとるがPythonを扱う、、（その20：Scapyで改ざん）](https://qiita.com/infinite1oop/items/9ee5e979c31b2028db5d)」参照。

### 定義
```python
### Main ###
# Inside Terminal Information #
in_ip_addr1 = '192.168.10.1'
in_ip_port1 = 1111
in_ip_addr2 = '192.168.10.2'
in_ip_port2 = 2222

# NAT Table List
nat_table1 = {}
nat_table2 = {}

# Transform List: IP address, Dest Port, NAT Table
trans_list = [[in_ip_addr1, in_ip_port1, nat_table1],\
              [in_ip_addr2, in_ip_port2, nat_table2]]

# NAT Outside & Inside Mac Address & IP Address
out_eth_mac = get_if_hwaddr('enp0s3')
out_eth_ip = get_if_addr('enp0s3')	# 10.1.1.254
in_eth_mac = get_if_hwaddr('enp0s8')
in_eth_ip = get_if_addr('enp0s8')	# 192.168.10.254
```
- 転送先IPアドレスおよびポート
- NATテーブル
- NATルーターのMacアドレスおよびIPアドレス取得

### ソケット定義、パケット受信
```python
# Layer2 socket
out_sock = conf.L2socket(iface='enp0s3')
in_sock = conf.L2socket(iface='enp0s8')

# Not send RST in order not to disconnect from NAT router
os.system('sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP')

while True:
    # select()
    read_sockets, _, _ = select.select([out_sock, in_sock], [], [])

    for s in read_sockets:
        # Data receive
        p = s.recv()

        # Drop non TCP packet
        if not p or not p.haslayer(TCP):
            #print("Not TCP packet")
            continue
```
- NAT外側・内側のソケット
- TCPセッションを維持するため、NATルーターからRSTを送信しない
    - これについても、上述した参考情報を参照
- selectによるパケット待受け
- データ受信（TCP以外は捨てる（何もしない））

### 外側からのパケット処理
```python
        # From Outside to Inside (Initiate from Outside)
        if p.dst == out_eth_mac:
            ip_pkt_out_to_in = p.getlayer(IP)
            found = 0
            for i in range(len(trans_list)):
                if ip_pkt_out_to_in.dport == trans_list[i][1]:
                    found = 1
                    # Keep Source IP and Source Port at NAT Table
                    trans_list[i][2][ip_pkt_out_to_in.src] = ip_pkt_out_to_in.sport
                    print(trans_list[i][2])
                    # Replace IP address with local IP adress
                    ip_pkt_out_to_in.dst = trans_list[i][0]
                    # Recreate IP address
                    pkt_to_ip_addr = recreate_ip_packet(ip_pkt_out_to_in)
                    print(pkt_to_ip_addr)
                    send(pkt_to_ip_addr, verbose=0)
                    break
            if found == 0:
                print("Closed Port: " + str(ip_pkt_out_to_in.dport))
```
- 宛先MacアドレスがNATルーターの外側
- IPパケットを取得（ip_pkt_out_to_int）
- 受信したパケットの宛先ポートがNATテーブルに含まれている場合
    - 送信側IPアドレスと送信ポートをテーブルに格納
    - 宛先IPアドレスを転送先IPアドレスに置き換え
    - 前述した関数recreate_ip_packet()によりIPパケットを再生成（pkt_to_ip_addr）
    - 送信
- 受信したパケットの宛先ポートがNATテーブルに含まれていない場合
    - ポートCloseのメッセージ表示

### 内側からのパケット処理
```python
        # From Inside to Outside
        elif p.dst == in_eth_mac:
            ip_pkt_in_to_out = p.getlayer(IP)
            found = 0
            for i in range(len(trans_list)):
                if ip_pkt_in_to_out.src == trans_list[i][0]:
                    found = 1
                    # Get destination port from destination IP at NAT Table
                    dport = trans_list[i][2].get(ip_pkt_in_to_out.dst)
                    #print(dport)
                    if dport is None:
                        print("No data in NAT Table")
                        continue
                    # Replace IP address with send IP address
                    ip_pkt_in_to_out.src = out_eth_ip
                    # Recreate IP address
                    pkt_from_ip_addr = recreate_ip_packet(ip_pkt_in_to_out)
                    print(pkt_from_ip_addr)
                    send(pkt_from_ip_addr, verbose=0)
                    break
            if found == 0:
                print("Not allowed inside IP address")
```
- 宛先MacアドレスがNATルーターの内側
- IPパケットを取得（ip_pkt_in_to_out）
- 送信先IPアドレスがNATテーブルに含まれている場合
    - 宛先ポートがテーブルに含まれているか確認
        - 内側起動のデータ通信はここでは考慮していない
    - 送信IPアドレスをNATルーター外側IPアドレスに置き換え
    - IPパケットを再生成（pkt_from_ip_addr）
    - 送信
- 送信先IPアドレスがNATテーブルに含まれていない場合
    - 登録されていないメッセージを表示

### 全体
```python
from scapy.all import *
import select
import os

# Recreate IP Packet #
def recreate_ip_packet(ip_pkt):
    # Removal Check-Sum
    if ip_pkt.haslayer(TCP):
        del ip_pkt.getlayer(TCP).chksum
    if ip_pkt.haslayer(UDP):
        del ip_pkt.getlayer(UDP).chksum
    if ip_pkt.haslayer(ICMP):
        del ip_pkt.getlayer(ICMP).chksum
    del ip_pkt.chksum

    result = IP(ip_pkt.build())
    return result

### Main ###
# Inside Terminal Information #
in_ip_addr1 = '192.168.10.1'
in_ip_port1 = 1111
in_ip_addr2 = '192.168.10.2'
in_ip_port2 = 2222

# NAT Table List
nat_table1 = {}
nat_table2 = {}

# Transform List: IP address, Dest Port, NAT Table
trans_list = [[in_ip_addr1, in_ip_port1, nat_table1],\
              [in_ip_addr2, in_ip_port2, nat_table2]]

# NAT Outside & Inside Mac Address & IP Address
out_eth_mac = get_if_hwaddr('enp0s3')
out_eth_ip = get_if_addr('enp0s3')	# 10.1.1.254
in_eth_mac = get_if_hwaddr('enp0s8')
in_eth_ip = get_if_addr('enp0s8')	# 192.168.10.254

# Layer2 socket
out_sock = conf.L2socket(iface='enp0s3')
in_sock = conf.L2socket(iface='enp0s8')

# Not send RST in order not to disconnect from NAT router
os.system('sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP')

while True:
    # select()
    read_sockets, _, _ = select.select([out_sock, in_sock], [], [])

    for s in read_sockets:
        # Data receive
        p = s.recv()

        # Drop non TCP packet
        if not p or not p.haslayer(TCP):
            #print("Not TCP packet")
            continue

        # From Outside to Inside (Initiate from Outside)
        if p.dst == out_eth_mac:
            ip_pkt_out_to_in = p.getlayer(IP)
            found = 0
            for i in range(len(trans_list)):
                if ip_pkt_out_to_in.dport == trans_list[i][1]:
                    found = 1
                    # Keep Source IP and Source Port at NAT Table
                    trans_list[i][2][ip_pkt_out_to_in.src] = ip_pkt_out_to_in.sport
                    print(trans_list[i][2])
                    # Replace IP address with local IP adress
                    ip_pkt_out_to_in.dst = trans_list[i][0]
                    # Recreate IP address
                    pkt_to_ip_addr = recreate_ip_packet(ip_pkt_out_to_in)
                    print(pkt_to_ip_addr)
                    send(pkt_to_ip_addr, verbose=0)
                    break
            if found == 0:
                print("Closed Port: " + str(ip_pkt_out_to_in.dport))

        # From Inside to Outside
        elif p.dst == in_eth_mac:
            ip_pkt_in_to_out = p.getlayer(IP)
            found = 0
            for i in range(len(trans_list)):
                if ip_pkt_in_to_out.src == trans_list[i][0]:
                    found = 1
                    # Get destination port from destination IP at NAT Table
                    dport = trans_list[i][2].get(ip_pkt_in_to_out.dst)
                    #print(dport)
                    if dport is None:
                        print("No data in NAT Table")
                        continue
                    # Replace IP address with send IP address
                    ip_pkt_in_to_out.src = out_eth_ip
                    # Recreate IP address
                    pkt_from_ip_addr = recreate_ip_packet(ip_pkt_in_to_out)
                    print(pkt_from_ip_addr)
                    send(pkt_from_ip_addr, verbose=0)
                    break
            if found == 0:
                print("Not allowed inside IP address")
```

## 検証
### 事前準備
Scapyによる転送を実施するため、NATルーターにてルーティングを無効化する。
```
$ sudo sysctl -w net.ipv4.ip_forward=0
```
### Port 1111へのフォワード
netcatによる確認。Server(192.168.10.1)にて、ループバックを実現する下記コマンド実行。
```
$ rm -f /tmp/f; mkfifo /tmp/f
$ cat /tmp/f | nc -l -p 1111 > /tmp/f
```
Client(10.1.1.1)で、相手先としてNATルーター外側IPアドレスを指定した時の実行の様子。
```
$ nc 10.1.1.254 1111
test
test
123
123
```
NATルーターで、指定された宛先である192.168.10.1へPort Forwardされ、"test"と入力して"test"がエコーバックしている。このときのNATルーターのログ（ソースコード：print）の状況は下記となる。
```
{'10.1.1.1': 52600}
IP / TCP 10.1.1.1:52600 > 192.168.10.1:1111 S
IP / TCP 10.1.1.254:1111 > 10.1.1.1:52600 SA
{'10.1.1.1': 52600}
IP / TCP 10.1.1.1:52600 > 192.168.10.1:1111 A
{'10.1.1.1': 52600}
IP / TCP 10.1.1.1:52600 > 192.168.10.1:1111 PA / Raw
IP / TCP 10.1.1.254:1111 > 10.1.1.1:52600 A
IP / TCP 10.1.1.254:1111 > 10.1.1.1:52600 PA / Raw
{'10.1.1.1': 52600}
IP / TCP 10.1.1.1:52600 > 192.168.10.1:1111 A
{'10.1.1.1': 52600}
IP / TCP 10.1.1.1:52600 > 192.168.10.1:1111 PA / Raw
IP / TCP 10.1.1.254:1111 > 10.1.1.1:52600 PA / Raw
{'10.1.1.1': 52600}
IP / TCP 10.1.1.1:52600 > 192.168.10.1:1111 A

```

### Port 2222へのフォワード
httpサーバーによる確認。Server(192.168.10.2)にて、待受けポート2222を指定し、httpサーバーを起動。Client（10.1.1.1および10.1.1.2）のブラウザにてアクセス。NATルーターで、指定された宛先である192.168.10.2へPort Forwardされ、HTTPレスポンスが得られる。
![BrowserAccess.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/59cb5eb3-f58e-f85f-addf-2e418131f2d0.png)
NATルーターのログの状況は下記となる。
```
{'10.1.1.1': 48826}
IP / TCP 10.1.1.1:48826 > 192.168.10.2:2222 S
IP / TCP 10.1.1.254:2222 > 10.1.1.1:48826 SA
{'10.1.1.1': 48826}
IP / TCP 10.1.1.1:48826 > 192.168.10.2:2222 A
{'10.1.1.1': 48826}
IP / TCP 10.1.1.1:48826 > 192.168.10.2:2222 PA / Raw
IP / TCP 10.1.1.254:2222 > 10.1.1.1:48826 A
IP / TCP 10.1.1.254:2222 > 10.1.1.1:48826 PA / Raw
IP / TCP 10.1.1.254:2222 > 10.1.1.1:48826 A / Raw
{'10.1.1.1': 48826}
IP / TCP 10.1.1.1:48826 > 192.168.10.2:2222 A
IP / TCP 10.1.1.254:2222 > 10.1.1.1:48826 FPA / Raw
{'10.1.1.1': 48826}
IP / TCP 10.1.1.1:48826 > 192.168.10.2:2222 A
{'10.1.1.1': 48826}
IP / TCP 10.1.1.1:48826 > 192.168.10.2:2222 FA
IP / TCP 10.1.1.254:2222 > 10.1.1.1:48826 A

{'10.1.1.1': 48826, '10.1.1.2': 59426}
IP / TCP 10.1.1.2:59426 > 192.168.10.2:2222 S
IP / TCP 10.1.1.254:2222 > 10.1.1.2:59426 SA
{'10.1.1.1': 48826, '10.1.1.2': 59426}
IP / TCP 10.1.1.2:59426 > 192.168.10.2:2222 A
{'10.1.1.1': 48826, '10.1.1.2': 59426}
IP / TCP 10.1.1.2:59426 > 192.168.10.2:2222 PA / Raw
IP / TCP 10.1.1.254:2222 > 10.1.1.2:59426 A
IP / TCP 10.1.1.254:2222 > 10.1.1.2:59426 PA / Raw
IP / TCP 10.1.1.254:2222 > 10.1.1.2:59426 A / Raw
{'10.1.1.1': 48826, '10.1.1.2': 59426}
IP / TCP 10.1.1.2:59426 > 192.168.10.2:2222 A
IP / TCP 10.1.1.254:2222 > 10.1.1.2:59426 FPA / Raw
{'10.1.1.1': 48826, '10.1.1.2': 59426}
IP / TCP 10.1.1.2:59426 > 192.168.10.2:2222 A
{'10.1.1.1': 48826, '10.1.1.2': 59426}
IP / TCP 10.1.1.2:59426 > 192.168.10.2:2222 FA
IP / TCP 10.1.1.254:2222 > 10.1.1.2:59426 A
```

### Port Forwardの対象ではないポートへのアクセス
Client（10.1.1.1）で下記実行。
```
$  nc 10.1.1.254 3333
```
NATルーターのログ。
```
Closed Port: 3333
```

### NAT内側からのアクセス
#### 未対象のIPアドレス（192.168.10.3）からの送信
```
$ echo | nc 10.1.1.1 3333
```
NATルーターのログ。
```
Not allowed inside IP address
```
#### 未登録状況でのIPアドレス（192.168.10.2）からの送信
```
$ echo 123 | nc 10.1.1.1 1234
```
NATルーターのログ。
```
No data in NAT Table
```


EOF