ゼロから自作したパケットをpythonのsocket通信を用いてサーバーに送信します。
SYNフラグのパケットをサーバーに送り、SYN, ACKフラグのパケットが返ってくるところまで確認します。
実行環境
$ cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=20.04 DISTRIB_CODENAME=focal DISTRIB_DESCRIPTION="Ubuntu 20.04.6 LTS" $ uname -srm Linux 5.4.0-156-generic x86_64
本文中コード: code-for-blogpost/raw_packet at main · nsakki55/code-for-blogpost · GitHub
目次
networkの作成
LinuxのNetwork Namespace機能を使って、図のような仮想ネットワークを作成します。 コマンドはLinuxで動かしながら学ぶTCP/IPネットワーク入門 を参考にしています。
Network Namespaceを作成します
$ sudo ip netns add ns1 $ sudo ip netns add ns2
2つのNetwork Namespaceを繋ぐvethインターフェイスを作成します
$ sudo ip link add ns1-veth0 type veth peer name ns2-veth0
vethインターフェイスをNetwork Namespaceに所属させます
$ sudo ip link set ns1-veth0 netns ns1 $ sudo ip link set ns2-veth0 netns ns2
ネットワークインターフェイスをupの状態に設定します
$ sudo ip netns exec ns1 ip link set ns1-veth0 up $ sudo ip netns exec ns2 ip link set ns2-veth0 up
IPアドレスを設定します
$ sudo ip netns exec ns1 ip address add 102.0.2.1/24 dev ns1-veth0 $ sudo ip netns exec ns2 ip address add 102.0.2.2/24 dev ns2-veth0
次にMACアドレスを設定します
$ sudo ip netns exec ns1 ip link set dev ns1-veth0 address 00:00:5E:00:53:01 $ sudo ip netns exec ns2 ip link set dev ns2-veth0 address 00:00:5E:00:53:02
IPアドレス192.0.2.2から192.0.2.1へpingコマンドを実行し、結果が返ってくることを確認できます
$ sudo ip netns exec ns2 ping -c 3 192.0.2.1 -I 192.0.2.2 PING 192.0.2.1 (192.0.2.1) 56(84) bytes of data. 64 bytes from 192.0.2.1: icmp_seq=1 ttl=64 time=1.20 ms 64 bytes from 192.0.2.1: icmp_seq=2 ttl=64 time=0.056 ms 64 bytes from 192.0.2.1: icmp_seq=3 ttl=64 time=0.052 ms --- 192.0.2.1 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2015ms rtt min/avg/max/mdev = 0.052/0.434/1.196/0.538 ms
serverの作成
作成したPacketを受信するためのサーバーを用意します。
socketライブラリのドキュメントに記載されているecho serverのコードを使用します。
socket --- 低水準ネットワークインターフェース — Python 3.11.5 ドキュメント
network namespace ns1で起動するserver.pyです
import socket HOST = '0.0.0.0' PORT = 54321 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind((HOST, PORT)) s.listen(1) conn, addr = s.accept() with conn: print('Connected by', addr) while True: data = conn.recv(1024) if not data: break conn.sendall(data)
サーバーを起動します
$ sudo ip netns exec ns1 python3 server.py
packetの作成
Ethernet フレーム
Destination MAC Address
送信元IPアドレス00:00:5e:00:53:02を16進数に変換すると、
00 00 5e 00 53 02
となります。MACアドレスが16進数表記なのでそのままの値です。
Source MAC Address
送信先IPアドレス00:00:5e:00:53:01を16進数に変換すると、
00 00 5e 00 53 01
となります。Type
16bit. イーサネットの上位層のプロトコル(=ネットワーク層のプロトコル)のタイプ
IEEEの検索サイトでタイプフィールドの一覧を確認できます。
InternetIP (IPv4)を使用するので
0800
となります。
作成したethernetフレームをPythonで表すと次のようになります。
ethernet_frame = b'\x00\x00\x5e\x00\x53\x01' # Destination MAC Address ethernet_frame += b'\x00\x00\x5e\x00\x53\x02' # Source MAC Address ethernet_frame += b'\x08\x00' # Protocol-Type
IP ヘッダ
上図のIPヘッダを作成します。各フィールドの値を設定していきます。
Version
4bit. IPヘッダのバージョン番号
IPのバージョン番号はIANAのサイトで確認できます。
IPv4を使用するので
4
となります。IHL
4bit. Internet Header Lengthの略. IPヘッダ自体の大きさ.単位は4オクテット(32bit)
オプションを持たないIPパケットの場合は 5 となります。
5 × 4オクテット = 20オクテットがオプション無しのIPパケットのヘッダ長。
今回はオプションを持たないので、
5
となります。DSCP, ECN
8bit. 送信してるIPパケットの優先度・輻輳制御を表す.DSCPが6bit, ECNが2bit使用.
DSCPはIPパケットの優先度を表します(RFC2474)。DSCPの値に応じてクラスセレクタ(CS)という名前がつけられています。
今回はBestEffortの0を用います。
(引用: https://ja.wikipedia.org/wiki/Type_of_Service)ECNは輻輳(ネットワークの混雑)制御の状態を表します(RFC3168)。2bitは2つの要素で構成されます。
今回はECN非対応とします。
DSCPとECNの値を合わせて、DSCP,ECNフィールドは16進数表記で
00
となります。Total Length
16bit. IPヘッダとIPデータを加えたパケット全体のオクテット長
IPヘッダの長さが20 byte, TCPヘッダの長さが20 byteです。今回はペイロードなしで送るので、20+20 = 40byteとなります。16進数表記で
0028
となります。Identification
16bit. パケットを分割した時、分割パケット(フラグメント)を復元するための識別子
今回は自分で決めた値
abcd
とします。Flags
3bit. パケットの分割(フラグメント)を制御するフラグ
3つのbitは次の意味を持ちます
- 0 bit: 未使用. 0 を指定する必要がある(Reserved)
- 1 bit: 分割してよいかを指示する (Don’t Fragment)
- 0: 分割可能
- 1: 分割不可能
- 2 bit: 分割されたパケットの場合、最後のパケットかを表す (More Fragment)
- 0: 最後のフラグメント
- 1: 途中のフラグメント
今回は分割可能不可能とします。bitで表現すると
010
となります。16進数で表記するため、次のFragment Offsetに合わせて16進数表記に変換します。
Fragment Offset
13bit. パケットが分割された場合、オリジナルデータのどこに位置していたか(オフセット)を表す
今回はパケット分割を行わないため、全て0とします。
16進数に変換するため、FlagsとFragment Offsetのbitを並べます。
Flagsは 010, Fragment Offsetは 0 0000 0000 0000 です。2つのフィールドを合わせたbitを16進数に変換すると
4000
となります。-
8bit. パケットが通過できるルーターの最大数.ルーターを通過するたびに 1 減少する
今回は64回とします。16進数表記は
40
です。 Protocol
8bit. IPヘッダの次のヘッダのプロトコルを表す
Protocolの一覧をIANAのサイトで確認できます。
今回はTCPを使用するので6となり、16進数表記で
06
となります。Header Checksum
16bit. IPヘッダにエラーがないかのチェック
IPヘッダ作成の最後に計算します。
Source Address
32bit. 送信元IPアドレス
送信元IPアドレス192.0.2.2を16進数に変換すると、
c0 00 02 02
となります。Destination Address
IPヘッダ checksumの計算
マスタリングTCP/IP入門編 第4章でchecksumの計算方法を次のように記しています。
まずチェックサムのフィールドを0にして、16ビット単位で1の補数の和を求めます。 そして、求まった値の1の補数をチェックサムフィールドに入れます。
自分はこの文章から具体的な計算方法を理解できませんでした。
こちらのブログ記事のchecksumの計算方法の説明が分かりやすいです。この記事を参考にしてchecksumの計算を行います。
【TCP/IP】チェックサムを計算していく - helloworlds
checksum計算手順
- checksumを0にして、IPヘッダの16進数を16bitずつ足していく
- 計算した16進数の和を2進数に変換する
- 16bitを超える(桁上がった)bitと、16bitで区切れた値を足す
1の補数にする
3で求めた2進数を反転させる。
反転させた2進数を16進数に変換する
1. checksumを0にして、IPヘッダの16進数を16bitずつ足していく
checksum以外の作成したIPヘッダの値を並べます
4: version 5: IHL 00: DSCP, ECN 0028: Total Length abcd: Identification 4000: Flags 40: TTL 06: Protocol 0000: checksum (0に設定する) c000 0202: Source Address c000 0201: Destination Address
16bit区切りで並べます。
4500 0028 abcd 4000 4006 0000 c000 0202 c000 0201
16bitずつ足し合わせます
4500 + 0028 + abcd + 4000 + 4006 + 0000 + c000 + 0202 + c000 + 0201 = 2f4fe
2. 計算した16進数の和を2進数に変換する
16進数 2f4fe 2進数 10 1111 0100 1111 1110
3. 16bitを超える(桁上がった)bitと、16bitで区切れた値を足す
16bitを超えた部分は 10 となり、16bitで区切れた値 1111 0100 1111 1110 に足し合わせます
10 + 1111 0100 1111 1110 = 1111 0101 0000 0000
4. 1の補数にする
補数については次のような値を意味します
「補数(complement)」とは、「元の数」と「補数」を足した場合に桁上がりが発生する数のうち「最小」の数のことです。
補数表現とは?1の補数と2の補数の違いと計算方法まとめ | サービス | プロエンジニア
2進数の場合、元の数を反転するだけで補数を求められます。
3で計算したbitを反転させます
1111 0101 0000 0000 ↓ ↓ ↓ ↓ 0000 1010 1111 1111
5. 反転させた2進数を16進数に変換する
0000 1010 1111 1111 → 0aff
IPヘッダのchecksumは 0aff
となります。
作成したIPヘッダをPythonで表すと次のようになります。
ip_header = b'\x45\x00\x00\x28' # Version, IHL, DSCP, ECN, Total Length ip_header += b'\xab\xcd\x40\x00' # Identification, Flags, Fragment Offset ip_header += b'\x40\x06\x0a\xff' # TTL, Protocol, Checksum ip_header += b'\xc0\x00\x02\x02' # Source Address ip_header += b'\xc0\x00\x02\x01' # Destination Address
TCP ヘッダ
上図のようなTCPヘッダを作成します。各フィールドの値を設定します。
Source Port
16bit. 送信元ポート番号
送信元ポート番号 12345 を16進数に変換すると、
30 39
となります。Destination Port
16bit. 送信先ポート番号
送信先ポート番号 54321 を16進数に変換すると、
d4 31
となります。Sequence Number
32bit. 送信データの順番や欠落を検出するためのシーケンス番号
- 初期値をランダムに設定
- 送信したデータのオクテット数だけ値をインクリメントする
- SYN, FIN パケットはデータを含んでいなくても1オクテット分インクリメントする
今回は初期値を0とし、16進数表記で
00 00 00 00
となります。Acknowledgement Number
32bit. 次に受診すべきデータのシーケンス番号
データ送信側は返された確認応答番号から正常に通信が行われたか確認できます。
今回は0とし、16進数表記で
00 00 00 00
となります。Data Offset
4bit. TCPヘッダの長さを表す.単位は4オクテット(32bit)
オプションを持たないTCPヘッダの場合は 5 となる。
5 × 4オクテット = 20オクテットがオプション無しのTCPヘッダ長となる。
今回はオプションを持たないため、ヘッダ長は16進数表記で
5
となります。Reserved
3bit. 将来の拡張のために用意されてるフィールド
0
が入ります。Control Flag
9bit. 制御ビット. RFC 3540 で追加されたNC(Nonce Sum)を含みます。
9つのbitは次の意味を持ちます
- 0 bit: NC. TCP拡張機能を使う
- 1 bit: CWR. 輻輳ウィンドウ減少. 輻輳ウィンドウ減少を通知
- 2 bit: ECE. ECN-Echo. 輻輳の発生を相手に通知
- 3 bit: URG. 緊急フラグ. 緊急データが含まれるセグメントを通知
- 4 bit: ACK. 応答確認フラグ. データ受信を相手に知らせる
- 5 bit: PSH. 転送強制フラグ. 受信したデータをすぐにアプリケーションに渡す
- 6 bit: RST. リセットフラグ. 異常検知した時に通信を中断する
- 7 bit: SYN. 同期フラグ. 通信の確立
- 8 bit: FIN. 転送終了フラグ. 通信の終了
SYNパケットを送るためSYNフラグを
1
とし、その他は0
とします。Data Offset, Reserved, Control Flagsのbit列をまとめて16進数表記にすると
50 02
となります。Window Size
16bit. 受信可能なデータサイズ(オクテット数)を表す
今回は適当に 64240 byteとし、16進数表記で
fa f0
となります。Checksum
16bit. TCPヘッダにエラーがないかのチェック
TCPヘッダ作成の最後に計算します。
Urgent Pointer
16bit. 緊急を要するデータが入るポインタを示す.Contorl FlagのURGが1のときに有効となる
今回はURGは0としてるので、Urgent Pointerは0となり、16進数表記で
00 00
となります。
TCPヘッダ checksumの計算
TCPヘッダのchecksumの計算には、TCP擬似ヘッダを使用します。
TCPヘッダのchecksumの計算はこちらの記事がわかりやすいです。
Windows計算機でTCPチェックサムを計算する方法:ITエンジニア兼きもの屋のフリーライフ:エンジニアライフ
checksumの計算手順
- TCP擬似ヘッダを作成
- TCP擬似ヘッダとchecksumを0にしたTCPヘッダを組み合わせる
- 組み合わせた16進数を16bitずつ足していく
- 計算した16進数の和を2進数に変換する
- 16bitを超える(桁上がった)bitと、16bitで区切れた値を足す
1の補数にする
5で求めた2進数を反転させる。
反転させた2進数を16進数に変換する
1. TCP擬似ヘッダを作成
TCP擬似ヘッダに必要な項目
c000 0202: Source Address c000 0201: Destination Address 00: padding 06: Protocol 0014: TCP Header length 5×4=20(10進数) → 14(16進数)
16bit区切りで並べます。これがTCP擬似ヘッダです。
c000 0202 c000 0201 0006 0014
2. TCP擬似ヘッダとchecksumを0にしたTCPヘッダを組み合わせる
TCPヘッダを作成します。
3039: Source Port d431: Destination Port 00000000: Sequence Number 00000000: Acknowledgement Number 5: Data Offset 002: Reserved, Control Flag faf0: Window Size 0000: checksum. 0を設定 0000: Urgent Pointer
16bit区切りで並べます。
3039 d431 0000 0000 0000 0000 5002 7110 0000 0000
c000 0202 c000 0201 0006 0014 3039 d431 0000 0000 0000 0000 5002 faf0 0000 0000
3. 組み合わせた16進数を16bitずつ足していく
c000 + 0202 + c000 + 0201 + 0006 + 0014 + 3039 + d431 + 0000 + 0000 + 0000 + 0000 + 5002 + faf0 + 0000 + 0000 = 3d379
4. 計算した16進数の和を2進数に変換する
16進数 3d379 2進数 11 1101 0011 0111 1001
5. 16bitを超える(桁上がった)bitと、16bitで区切れた値を足す
11 + 1101 0011 0111 1001 = 1101 0011 0111 1100
6. 1の補数にする
bitを反転し補数を求めます
1101 0011 0111 1100 ↓ ↓ ↓ ↓ 0010 1100 1000 0011
7. 反転させた2進数を16進数に変換する
0010 1100 1000 0011 → 2c83
TCPヘッダのchecksumは 2c 83
となります。
作成したTCPヘッダをPythonで表すと次のようになります。
tcp_header = b'\x30\x39\xd4\x31' # Source Port, Destination Port tcp_header += b'\x00\x00\x00\x00' # Sequence Number tcp_header += b'\x00\x00\x00\x00' # Acknowledgement Number tcp_header += b'\x50\x02\xfa\xf0' # Data Offset, Reserved, Flags, Window Size tcp_header += b'\x2c\x83\x00\x00' # Checksum, Urgent Pointer
自作Packetを送信する
自作パケットをsocket通信で送信します。
socket通信で生パケットを送るには、socket作成時に socket.SOCK_RAW
を渡します。
bindにはnetwork namespce ns2のネットワークインターフェイスを指定します。
import socket s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW) s.bind(("ns2-veth0", 0)) ethernet_frame = b'\x00\x00\x5e\x00\x53\x01' # Destination MAC Address ethernet_frame += b'\x00\x00\x5e\x00\x53\x02' # Source MAC Address ethernet_frame += b'\x08\x00' # Protocol-Type ip_header = b'\x45\x00\x00\x28' # Version, IHL, DSCP, ECN, Total Length ip_header += b'\xab\xcd\x40\x00' # Identification, Flags, Fragment Offset ip_header += b'\x40\x06\x0a\xff' # TTL, Protocol, Checksum ip_header += b'\xc0\x00\x02\x02' # Source Address ip_header += b'\xc0\x00\x02\x01' # Destination Address tcp_header = b'\x30\x39\xd4\x31' # Source Port, Destination Port tcp_header += b'\x00\x00\x00\x00' # Sequence Number tcp_header += b'\x00\x00\x00\x00' # Acknowledgement Number tcp_header += b'\x50\x02\xfa\xf0' # Data Offset, Reserved, Flags, Window Size tcp_header += b'\x2c\x83\x00\x00' # Checksum, Urgent Pointer packet = ethernet_frame + ip_header + tcp_header s.send(packet)
通信結果を確認するため、tcpdumpでnamespace ns1 の通信内容を保存します
$ sudo ip netns exec ns1 tcpdump -w result -tnl -vv 'tcp'
自作した16進数表記のEthernetフレーム・IPヘッダ・TCPヘッダを結合したPacketを送ります。
$ sudo ip netns exec ns2 python3 raw_packet.py
Wiresharkで通信を確認
IPアドレス192.0.2.2のクライアントから192.0.2.1のサーバーに対してSYNパケットが送られています。サーバーからは3-way-handshakeのSYN, ACKパケットが返ってきています。
クライアント側ではSYN, ACKパケットを扱う機能を用意していないため、通信を中断するRSTパケットが送られています。
SYNパケットの内容を確認します。
Ethernetフレーム、IPヘッダ、TCPヘッダの各種フィールドに自分で設定した値が入ってることを確認できます。
Frame 1: 54 bytes on wire (432 bits), 54 bytes captured (432 bits) Ethernet II, Src: ICANNIAN_00:53:02 (00:00:5e:00:53:02), Dst: ICANNIAN_00:53:01 (00:00:5e:00:53:01) Destination: ICANNIAN_00:53:01 (00:00:5e:00:53:01) Address: ICANNIAN_00:53:01 (00:00:5e:00:53:01) .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default) .... ...0 .... .... .... .... = IG bit: Individual address (unicast) Source: ICANNIAN_00:53:02 (00:00:5e:00:53:02) Address: ICANNIAN_00:53:02 (00:00:5e:00:53:02) .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default) .... ...0 .... .... .... .... = IG bit: Individual address (unicast) Type: IPv4 (0x0800) Internet Protocol Version 4, Src: 192.0.2.2, Dst: 192.0.2.1 0100 .... = Version: 4 .... 0101 = Header Length: 20 bytes (5) Differentiated Services Field: 0x00 (DSCP: CS0, ECN: Not-ECT) 0000 00.. = Differentiated Services Codepoint: Default (0) .... ..00 = Explicit Congestion Notification: Not ECN-Capable Transport (0) Total Length: 40 Identification: 0xabcd (43981) 010. .... = Flags: 0x2, Don't fragment 0... .... = Reserved bit: Not set .1.. .... = Don't fragment: Set ..0. .... = More fragments: Not set ...0 0000 0000 0000 = Fragment Offset: 0 Time to Live: 64 Protocol: TCP (6) Header Checksum: 0x0aff [validation disabled] [Header checksum status: Unverified] Source Address: 192.0.2.2 Destination Address: 192.0.2.1 Transmission Control Protocol, Src Port: 12345, Dst Port: 54321, Seq: 0, Len: 0 Source Port: 12345 Destination Port: 54321 [Stream index: 0] [Conversation completeness: Incomplete (35)] [TCP Segment Len: 0] Sequence Number: 0 (relative sequence number) Sequence Number (raw): 0 [Next Sequence Number: 1 (relative sequence number)] Acknowledgment Number: 0 Acknowledgment number (raw): 0 0101 .... = Header Length: 20 bytes (5) Flags: 0x002 (SYN) Window: 64240 [Calculated window size: 64240] Checksum: 0x2c83 [correct] [Checksum Status: Good] [Calculated Checksum: 0x2c83] Urgent Pointer: 0 [Timestamps]
以上、自作パケットをsocket通信でサーバーに送ることができました。