Open4

「RUSTで始めるTCP自作入門」メモ

h-izuh-izu

TCPセグメントの定義

https://datatracker.ietf.org/doc/html/rfc793#section-3.1

最初の16バイトが送信元ポートになっている。次の16バイトが送信先ポート。104ビット(13バイト)からTCPフラグ。

ビッグエンディアンで値を入れないといけないので、u16::to_be_bytesでビッグエンディアンに変換して入れる。

u16::to_be_bytes
https://doc.rust-lang.org/std/primitive.u16.html#method.to_be_bytes

逆にビッグエンディアンのデータを受け取るときはu16::from_be_bytesを使う
https://doc.rust-lang.org/std/primitive.u16.html#method.from_be_bytes

TCPステータス

https://datatracker.ietf.org/doc/html/rfc793#section-3.2

TCP Connection State Diagram Figure 6.

ToyTcpで動くスレッド

  • 送信スレッド(メインスレッド)
  • 受信スレッド
  • 再送管理用のタイマースレッド

の3つのスレッドが動く。condition variableを使って各スレッドでステータスを変更しながら処理が進む

3 way handshakeの流れ

  1. (送信スレッド) SYNを送り、自身のステータスをSYNSENTに変える
  2. (送信スレッド) wait_eventでイベント(ConnectionCompleted)を待つ
  3. (受信スレッド) SYN/ACKを受け取り、自身のステータスをESTABLISHEDに変える
  4. (受信スレッド) イベント(ConnectionCompleted)をpublishする
  5. (送信スレッド) イベント(ConnectionCompleted)によってスレッドが起こされて処理が完了する
h-izuh-izu

send_paramとrecv_param

send_paramは自分のシーケンス番号を管理するのための構造体で、recv_paramは相手のシーケンス番号をシミュレート(模倣)して返すべきACK番号を知るための構造体というイメージ

基本的に次のような流れで更新される

  1. Aのsend_param.nextはS
  2. Aがデータをxバイト送る(packetのシーケンス番号はS)
  3. Aのsend_param.nextがx増える
  4. Bがデータをxバイト受信する
  5. Bのrecv_param.nextがx増える
  6. AがACKを受け取る
  7. Aのsend_param.unacked_seqをACKの番号にする

3 way handshake

plantuml
@startuml
participant EchoClient
participant "<<main thread>>\nclient" as client
participant "<<receive thread>>\nclient" as r_client
participant "<<timer thread>>\nclient" as t_client
participant "<<main thread>>\nserver" as server
participant "<<receive thread>>\nserver" as r_server
participant "<<timer thread>>\nserver" as t_server
participant EchoServer

EchoServer -> server: listen
EchoServer -> server :accept
server -> server :wait_event(ConnectionCompleted)
activate server
note over server
LISTEN状態
end note
EchoClient -> client: connect
client -> client: send_tcp_packet(SYN)
note right
1. send_params.init_seqをランダムに決定する
2. packetのseqはsend_params.init_seqにする
3. send_params.nextを send_params.seq + 1に設定する
3. send_params.unacked_seqを send_params.seqに設定する

|param|value|
|--|--|
|send_param.init_seq | 1000(random)|
|send_param.unacked_seq | send_param.init_seq |
|send_param.next | send_param.init_seq  + 1|
|send_param.window | 3800(default)|
|recv_param.init_seq | |
|recv_param.next| |
|recv_param.window|3800(default)|

packet
|SEQ|ACK|WINDOW|
|--|--|--|
|send_param.init_seq |0|recv_param.window|
end note
note over client
SYNSENT状態
end note
client --> r_server :listen_handler
note over r_server
1. listenソケットとは別のデータ送受信用のソケットを作成する(SYNRVCD状態)
2. recv_param.next = packet.get_seq() + 1
3.send_param.ini_seqをランダムに決定する
4. send_param.unacked_seqをsend_param.init_seqにする
4. send_param.nextをsend_param.init_seqにする

|param|value|
|--|--|
|send_param.init_seq | 2000(random)|
|send_param.unacked_seq | **send_param.init_seq** |
|send_param.next | **send_param.init_seq + 1**|
|send_param.window | 3800(default)|
|recv_param.init_seq | **packet.SEQ(1000)**|
|recv_param.next| **packet.SEQ(1000) + 1**|
|recv_param.window| **packet.WINDOW(3800)** |

packet
|SEQ|ACK|WINDOW|
|--|--|--|
|send_param.init_seq |recv_param.next |recv_param.window|
end note
r_server -> r_server :send_tcp_packet(SYN/ACK)
note over r_server
SYNRCVD状態
end note
r_server --> r_client :synsent_handler
note over r_client
1. send_param.unacked_seqをpacketのACKにする
2. recv_param.init_seqをpacketのseqにする
3. recv_param.nextをpacketのseq + 1にする

|param|value|
|--|--|
|send_param.init_seq | 1000|
|send_param.unacked_seq | **1000 -> packet.ACK(1001)**  |
|send_param.next | send_param.init_seq  + 1|
|send_param.window | **packet.WINDOW(3800)** |
|recv_param.init_seq | **packet.SEQ(2000)**|
|recv_param.next| **packet.SEQ(2000) + 1**|
|recv_param.window|3800(default)|

packet
|SEQ|ACK|WINDOW|
|--|--|--|
| **send_param.next(1001)** |**recv_param.next(2001)**|recv_param.window|
end note
r_client -> r_client :send_tcp_packet(ACK)
note over r_client
ESTABLISHED状態
end note
r_client --> r_server :synrcvd_handler
note right
1. send_param.unacked_seqをpacketのACKにする
2. recv_param.nextをpacketのseqにする

|param|value|
|--|--|
|send_param.init_seq | 2000 |
|send_param.unacked_seq | **2000 -> packet.ACK(2001)** |
|send_param.next | send_param.init_seq + 1|
|send_param.window | 3800 |
|recv_param.init_seq | 1000 |
|recv_param.next| 1001 |
|recv_param.window| 3800 |
end note
note over r_server
ESTABLISHED状態
end note
r_server -> r_server: publish_event(ConnectionCompleted)
server -> EchoServer: Ok(sock_id)
deactivate server
h-izuh-izu

ペイロードの受信

送る側はsend_paramを、受け取る側はrecv_paramを更新する。

send_paramはパケットのACK番号で、recv_paramはパケットのシーケンス番号をもとに更新される。

plantuml
@startuml
participant EchoClient
participant "<<main thread>>\nclient" as client
participant "<<receive thread>>\nclient" as r_client
participant "<<timer thread>>\nclient" as t_client
participant "<<main thread>>\nserver" as server
participant "<<receive thread>>\nserver" as r_server
participant "<<timer thread>>\nserver" as t_server
participant EchoServer

=== 3 way handshake 終了 ==

EchoServer -> server :recv(&buffer)
server -> server :wait_event(DataArrived)
activate server
EchoClient -> client :send('aaaaa')
note right
1. packetのseqはsend_param.nextにする
2. send_param.nextを6増やす
3. send_param.windowを6減らす

|param|value|
|--|--|
|send_param.init_seq | 1000|
|send_param.unacked_seq | 1001 |
|send_param.next | **1001 -> 1007** |
|send_param.window | **3800 -> 3874**|
|recv_param.init_seq | 2000 |
|recv_param.next| 2001 |
|recv_param.window| 3800 |

packet
|SEQ|ACK|WINDOW|
|--|--|--|
|send_param.next(1001) |recv_param.next|recv_param.window|
end note
client -> client :send_tcp_packet()
client --> r_server :established_handler
r_server -> r_server :process_payload()
note left
1. payloadの長さを計算する(6 bytes)
2. recv_param.nextをpacketの(seq + 6)にする
3. recv_param.windowを6減らす

|param|value|
|--|--|
|send_param.init_seq | 2000 |
|send_param.unacked_seq | 2001 |
|send_param.next | 2001 |
|send_param.window | 3800 |
|recv_param.init_seq | 1000 |
|recv_param.next| **1001 -> 1007** |
|recv_param.window| **3800 -> 3874 ** |

packet
|SEQ|ACK|WINDOW|
|--|--|--|
|send_param.next(2001) |**recv_param.next(1007)**|recv_param.window|

end note
r_server -> r_server :send_tcp_packet(ACK)
note left
packetのackをrecv_params.nextにする
end note
r_server --> server :publish_event(DataArrived)
server -> EchoServer :受信したデータをbufferにコピーする
deactivate server
note left
1. recv_bufferの先頭から
(recv_buffer.len() - recv_params.window) = 6 bytes
のデータをbufferにcopyする
2. recv_params.windowを6増やす

|param|value|
|--|--|
|send_param.init_seq | 2000 |
|send_param.unacked_seq | 2001 |
|send_param.next | 2001 |
|send_param.window | 3800 |
|recv_param.init_seq | 1000 |
|recv_param.next| 1007 |
|recv_param.window| **3874 -> 3800** |
end note
r_server --> r_client :established_handler
note left
unacked_seqをpacketのackに更新する

|param|value|
|--|--|
|send_param.init_seq | 1000|
|send_param.unacked_seq | **1001 -> packet.ACK(1007)** |
|send_param.next | 1007 |
|send_param.window | 3874|
|recv_param.init_seq | 2000 |
|recv_param.next| 2001 |
|recv_param.window| 3800 |
end note
t_client -> t_client :ack済みのセグメントを削除
note left
send_params.windowを6増やす
end note
@enduml
h-izuh-izu

チェックサム

この記事が詳しい
https://alpha-netzilla.blogspot.com/2011/08/network.html

pnetの実装
https://docs.rs/pnet_packet/0.34.0/src/pnet_packet/util.rs.html#92-99

ソースアドレスなどを1wordの値をして足していく。TCPのペイロード部分も同じように足していく

https://docs.rs/pnet_packet/0.34.0/src/pnet_packet/util.rs.html#100

/// Calculate the checksum for a packet built on IPv4. Used by UDP and TCP.
pub fn ipv4_checksum(
    data: &[u8],
    skipword: usize,
    extra_data: &[u8],
    source: &Ipv4Addr,
    destination: &Ipv4Addr,
    next_level_protocol: IpNextHeaderProtocol,
) -> u16be {
    let mut sum = 0u32;

    // Checksum pseudo-header
    sum += ipv4_word_sum(source);
    sum += ipv4_word_sum(destination);

    let IpNextHeaderProtocol(next_level_protocol) = next_level_protocol;
    sum += next_level_protocol as u32;

    let len = data.len() + extra_data.len();
    sum += len as u32;

    // Checksum packet header and data
    sum += sum_be_words(data, skipword);
    sum += sum_be_words(extra_data, extra_data.len() / 2);

    finalize_checksum(sum)
}

TCPペイロードの部分はビッグエンディアンで入っているとして扱う。16ビット毎に見てsumにu16の値を足していく。skipwordは8になっていてここはchecksumが入る場所で、データを送るときはここを0として扱ってチェックサムを計算する。

https://docs.rs/pnet_packet/0.34.0/src/pnet_packet/util.rs.html#158

/// Sum all words (16 bit chunks) in the given data. The word at word offset
/// `skipword` will be skipped. Each word is treated as big endian.
fn sum_be_words(data: &[u8], skipword: usize) -> u32 {
    if data.len() == 0 {
        return 0;
    }
    let len = data.len();
    let mut cur_data = &data[..];
    let mut sum = 0u32;
    let mut i = 0;
    while cur_data.len() >= 2 {
        if i != skipword {
            // It's safe to unwrap because we verified there are at least 2 bytes
            sum += u16::from_be_bytes(cur_data[0..2].try_into().unwrap()) as u32;
        }
        cur_data = &cur_data[2..];
        i += 1;
    }

    // If the length is odd, make sure to checksum the final byte
    if i != skipword && len & 1 != 0 {
        sum += (data[len - 1] as u32) << 8;
    }

    sum
}

https://docs.rs/pnet_packet/0.34.0/src/pnet_packet/util.rs.html#84
32ビットの値を16bit毎に分けて足し算をして、1の補数をとる

fn finalize_checksum(mut sum: u32) -> u16be {
    while sum >> 16 != 0 {
        sum = (sum >> 16) + (sum & 0xFFFF);
    }
    !sum as u16
}