肉球でキーボード

MLエンジニアの技術ブログです

パケットをゼロから自作してPythonのsocket通信で送る


ゼロから自作したパケットを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機能を使って、図のような仮想ネットワークを作成します。

network構成
コマンドは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 フレーム

作成した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ヘッダ

上図の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, ECN

    DSCPはIPパケットの優先度を表します(RFC2474)。DSCPの値に応じてクラスセレクタ(CS)という名前がつけられています。
    今回はBestEffortの0を用います。


    (引用: https://ja.wikipedia.org/wiki/Type_of_Service)

    ECNは輻輳(ネットワークの混雑)制御の状態を表します(RFC3168)。2bitは2つの要素で構成されます。

    • ECT: 上位層のプロトコルがECNに対応してるか
    • CE: ネットワーク機器が輻輳状態の場合1となる

    今回は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とします。

    Flags, Fragment Offset

    16進数に変換するため、FlagsとFragment Offsetのbitを並べます。

    Flagsは 010, Fragment Offsetは 0 0000 0000 0000 です。2つのフィールドを合わせたbitを16進数に変換すると 4000 となります。

  • TTL

    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

    32bit. 送信先IPアドレス

    送信先IPアドレス192.0.2.1を16進数に変換すると、 c0 00 02 01 となります。

IPヘッダ checksumの計算

マスタリングTCP/IP入門編 第4章でchecksumの計算方法を次のように記しています。

まずチェックサムのフィールドを0にして、16ビット単位で1の補数の和を求めます。
そして、求まった値の1の補数をチェックサムフィールドに入れます。

自分はこの文章から具体的な計算方法を理解できませんでした。

こちらのブログ記事のchecksumの計算方法の説明が分かりやすいです。この記事を参考にしてchecksumの計算を行います。

【TCP/IP】チェックサムを計算していく - helloworlds

checksum計算手順

  1. checksumを0にして、IPヘッダの16進数を16bitずつ足していく
  2. 計算した16進数の和を2進数に変換する
  3. 16bitを超える(桁上がった)bitと、16bitで区切れた値を足す
  4. 1の補数にする

    3で求めた2進数を反転させる。

  5. 反転させた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ヘッダ

上図のような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 となります。

    Data Offset, Reserved, Control Flags

  • 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の計算手順

  1. TCP擬似ヘッダを作成
  2. TCP擬似ヘッダとchecksumを0にしたTCPヘッダを組み合わせる
  3. 組み合わせた16進数を16bitずつ足していく
  4. 計算した16進数の和を2進数に変換する
  5. 16bitを超える(桁上がった)bitと、16bitで区切れた値を足す
  6. 1の補数にする

    5で求めた2進数を反転させる。

  7. 反転させた2進数を16進数に変換する

1. TCP擬似ヘッダを作成

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

TCP擬似ヘッダとTCPヘッダを組み合わせます

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で通信を確認

wiresharktcpdumpした結果を表示します。

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通信でサーバーに送ることができました。

参考