Unix Network Programming: Chap4

Oct 10, 2022 22:14 · 1113 words · 6 minute read

4.基本TCPソケット 🔗

Goal 🔗

  • ソケット関数について理解する(実装は次の章)。
  • 複数クライアントが同時に接続する並行サーバーについて理解する。
    • ただし、ここでは fork を使った 1プロセス1クライアントモデル のみを考える。

socket関数 🔗

  • サーバー/クライアントいずれも、ネットワークI/Oを行うために最初に行うのは socket(family, type, protocol) 関数を呼び出して特定のプロトコルのソケットを作成すること。
  • プロトコルファミリ
    • AF_INET: IPv4プロトコル
    • AF_INET6: IPv6プロトコル
    • AF_LOCAL: Unixドメインプロトコル
    • AF_ROUTE: 経路制御ソケット
    • AF_KEY: キーソケット
  • ソケットタイプ
    • SOCK_STREAM: ストリームソケット
    • SOCK_DGRAM: データグラムソケット
    • SOCK_RAW: raw ソケット
  • プロトコルには、raw ソケット以外原則 0 (詳細は25章)
AF_INET AF_INET6 AF_LOCAL AF_ROUTE AF_KEY
SOCK_STREAM TCP TCP Yes N/A N/A
SOCK_DGRAM UDP UDP Yes N/A N/A
SOCK_RAW IPv4 IPv6 N/A Yes Yes

AF_LOCALAF_UNIX とされる場合もある。

  • socket 関数は、成功すると小さな非負整数を返す(ソケットディスクリプタ: sockfd)。
  • socket 関数を呼び出すタイミングでは、まだローカル/リモートアドレスを指定しない。
  • AF_/PF_ プレフィックス
    • AF_: アドレスファミリを表す。
    • PF_: プロトコルファミリを表す。
    • もともとは1つのプロトコルファミリが、複数のアドレスファミリをサポートすることを想定していた。
      • ソケットの作成には PF_ が使われ、ソケットアドレス構造体には AF_ が使われてきた。
    • 実際には、複数のアドレスファミリをサポートするプロトコルファミリは登場しない。

connect 関数 🔗

  • TCPクライアントが、TCPサーバーとのコネクションを確立するために用いられる。
  • int connect(sockfd, sockaddr*, addrlen)
    • 成功なら0、エラーなら1を返す。
  • クライアントは、connectに先立ってbindを呼び出す必要はない。
    • カーネルが必要に応じてエフェメラルポートを選択する。
  • connectを呼び出すことで、3way handshake が開始される。
    • コネクションが確立するか、エラーが発生するまで制御は戻らない。
  • エラーになるパターン:
    • SYNセグメントに対する応答を受信しなかった場合 ETIMEDOUT が返される。
      • 4.4BSD では、6秒後、24秒後に SYN を再送し、75秒後にエラーとなる。
    • SYNセグメントに対して RST が応答された場合、ECONNREFUSED が返される。
      • サーバーホスト上の指定したポートでコネクションを待っているプロセスが存在しないことを意味する。
    • 中間のルーターで、ICMP終点到達不可エラーが発生した場合、EHOSTUNREACHまたはENETUNREACHが返される。
  • connect が失敗した場合、ソケットクローズされなければならない(再利用は不可)。

bind 関数 🔗

  • ソケットにローカルプロトコルアドレスを紐づける。
    • 32bit IPv4アドレスまたは 128bit IPv6アドレス。
  • もし bind せずに listen を開始した場合、カーネルによってエフェメラルポートが選択される。
    • ただし、一般的にはサーバーがエフェメラルポートを利用する事態は稀。
      • 例) RPC: カーネルに選択させたポートを、クライアントに UDP で問い合わさせる。
  • もし IP アドレスを指定せずに bind した場合、カーネルはクライアントからの SYN に含まれる終点IPを、サーバー側の始点IPとして選択する。
  • IPv4 では、IPアドレスを指定しない場合にワイルドカードとしてINADDR_ANY定数を使う。
  • エフェメラルポートの選択をカーネルに指示した場合、ポートの値を知るためには getsockname 関数を使う。
  • 非ワイルドカードIPをバインドしたくなるケース: 複数の組織に Web サーバーを提供
    • 同一サブネット 198.69.10 に 2つの組織 198.69.10.128198.69.10.129 が存在する場合。
    • 同一のインターフェースに、エイリアスとして2つのIPアドレスを紐づける形になる。
    • サーバープロセスは、組織ごとに起動され、それぞれ 198.69.10.128198.69.10.129 のみをバインドする。
      • 1つのプロセスは、どちらか片方の組織からのリクエストのみを受け付ける。

listen 関数 🔗

  • socket 関数で作られたソケットは、アクティブ(クライアント)ソケットと見做される。
  • listen 関数を呼び出すことで、パッシブソケットに変換する。
  • 第2引数(backlog)でコネクションキューの最大長を設定する。
  • bind 関数を呼び出してから、accept 関数を呼び出すまでに listen が呼び出される。
  • 各リスニングソケットに対して、2種類のキューが存在する。
    • 確立待ちコネクションキュー(incomplete connection queue)
      • クライアントから到着したSYNに対応する。
      • 3way handshake の完了待ちエントリ。
    • 確立済みコネクションキュー(completed connection queue)
      • 3way handshake の完了済みエントリ。
      • ESTABLISHED な状態。
    • これら2つのキューの長さの合計が、backlog を超えない。
  • クライアントから SYN が到着すると、確立待ちコネクションキューに入れられる。
  • その後クライアントから ACK が到着すると、確立済みコネクションキューに入れられる。
  • accept 関数を呼び出すと、確立済みコネクションキューの先頭がプロセスに渡される。
    • accept を呼び出した時に、確立済みコネクションキューがからの場合、プロセスはスリープ状態に入る。
  • クライアントから SYN が到着した時にキューが満杯だった場合、その SYN は無視される。
    • RST が送られることはない。
    • クライアントは SYN を再送することで、キューに含まれることを期待する。
  • 確立済みコネクションキューに入っている間にクライアントから送られてきたデータは、可能な限りサーバー側で保持しておくのが望ましい。
  • キューを狙った攻撃: SYN flooding
    • 高レートで SYN を送出するようなプログラムを書き、ターゲットの確立待ちコネクションキューを溢れさせる。
    • 送られる SYN の始点IPアドレス(source address)にはランダムな値が格納されることで、サーバーからの応答(SYN+ACK)はどこにも届かない(IP spoofing)。
      • 攻撃者の IP アドレスが特定されない。

accept 関数 🔗

  • 確立済みコネクションキューの先頭から、コネクションを取り出す。
  • accept(sockfd, *cliaddr, *addrlen)
  • 第2引数に渡したソケットアドレス構造体の参照に、クライアントのプロトコルアドレスの情報を格納してくれる。
  • 成功したら、カーネルが新たに作成したクライアントとのTCPコネクションを参照する非負のディスクリプタを返す。
  • つまり、accept の第1引数はリスニングソケット、戻り値は接続済みソケットとなる。
  • サーバー側の標準出力にクライアントのIP/Port を出力する daytime サーバー。
#include "unp.h"
#include <time.h>

int main(int argc, char **argv) {
    int                listenfd, connfd;
    socklen_t          len;
    struct sockaddr_in servaddr, cliaddr;
    char               buff[MAXLINE];
    time_t             ticks;

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family      = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port        = htons(13);
    Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
    Listen(listenfd, LISTENQ);
    for(;;) {
        len    = sizeof(cliaddr);
        connfd = Accept(listenfd, (SA *)&cliaddr, &len);
        printf("connection from %s, port %d\n", Inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)), ntohs(cliaddr.sin_port));
        ticks = time(NULL);
        snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
        Write(connfd, buff, strlen(buff));
        Close(connfd);
    }
}

fork関数とexec関数 🔗

  • Unix において新しいプロセスを生成するための唯一の方法が fork
  • 1度呼び出されると、2度制御を返す。
    • 自分が子プロセスとして動いているなら、戻り値0
    • 自分が親プロセスとして動いているなら、戻り値<子プロセスのID>
    • 親プロセスのIDをgetppidで取得できることから、子プロセスの戻り値が0になる妥当性がわかる。
  • fork前に親プロセスで開いていたディスクリプタは、すべて子プロセスと共有される。
    • 親プロセスで accept した後に fork し、親プロセス側のみ close する。
  • 並行サーバーでは、forkしたあとの処理も同じプログラムに含まれるのが一般的。
  • 一方でシェルでは、forkしたあとに別のプログラムになることが一般的。
    • この時使われるのが exec。
  • exec は以下の6通りの組み合わせ
    • 「pathを指定するか、ファイル名を指定するか」
    • 「引数を1つずつ並べるか、配列で渡すか」
    • 「呼び出しプロセスの環境をそのまま使うか、新しい環境にするか」
    • (うち2通りは存在しない)
  • exec のI/Fは6通りあるものの、実際のシステムコールは execve(*pathname, *argv[], *envp[]) のみで、それ以外はラッパー。
  • 2章では、TCPソケットに対する close が終了シーケンスを起動するとのことだったが、親プロセスがTCPソケットをクローズしても終了シーケンスは起動しない。
    • すべてのファイル/ソケットは、参照カウントを持っている。
    • OSが管理するファイルテーブルでは、あるファイルに対するディスクリプタの数が記録されている。
    • TCPソケットの終了シーケンスが起動するのは、参照の数が0になったとき。
    • または shutdown 関数によって、明示的に終了シーケンスを起動させることができる。

close関数 🔗

  • close(sockfd)
  • ソケットのクローズを記録し、即座に制御が戻る。
  • プロセスは、以降ディスクリプ他を使用すrことができなくなる。
  • 親プロセスが、accept したソケットディスクリプタを close しない場合の問題。
    • ディスクリプタの枯渇。
    • 子プロセス側で close しても、終了シーケンスが起動しない。

getsockname関数とgetpeername関数 🔗

  • getsockname は、ソケットのローカル(自分側)プロトコルアドレスを返す。
  • getpeername は、ソケットのリモート(相手側)プロトコルアドレスを返す。
  • いつ使うのか。
    • クライアント側で、自分自身のプロトコルアドレスを確認するケース。
    • ワイルドカードIP、ポートを指定したサーバーで、自分自身のプロトコルアドレスを確認するケース。
      • ただし、accept した後に実行する必要がある。

横道 🔗

  • tcpdump で daytime の通信をのぞいてみる。
    • ローカルで daytime サーバーを起動しておく。
    • tcpdump -i lo -X port 13: ループバックインターフェースの Port 13 の通信を監視する。
    • ローカルで daytime クライアントを起動する。
      • 結果: Sun Oct 9 18:31:46 2022
    • tcpdump の出力
      • パケット1: IP 127.0.0.1.36162 > 127.0.0.1.13: Flags [S], seq 1007334733, win 65495, options [mss 65495,sackOK,TS val 2329055490 ecr 0,nop,wscale 7], length 0
        • クライアントにはエフェメラルポート 36162 が割り当てられている。
        • SYN であることを表す [S] フラグが立っている。
        • シーケンス番号は 1007334733
        • window size は 65,495バイト。
        • ペイロードは 0バイト。
      • パケット2: IP 127.0.0.1.13 > 127.0.0.1.36162: Flags [S.], seq 3871193739, ack 1007334734, win 65483, options [mss 65495,sackOK,TS val 2329055491 ecr 2329055490,nop,wscale 7], length 0
        • サーバーから、パケット1に対する応答として ACK+SYN が返ってきている。
        • パケット1のシーケンス番号に+1した値が ack 番号に入っている。
        • SYN のシーケンス番号は 3871193739
      • パケット3: IP 127.0.0.1.36162 > 127.0.0.1.13: Flags [.], ack 1, win 512, options [nop,nop,TS val 2329055491 ecr 2329055491], length 0
        • クライアントがサーバーのSYNに応答している。
        • ack 番号が 1 になっているが、これは tcpdump が見やすくしてくれているため。
          • 実際は 3871193739+1
          • tcpdump を -S フラグをつけて起動すると確認できる。
      • パケット4: IP 127.0.0.1.13 > 127.0.0.1.36162: Flags [P.], seq 1:27, ack 1, win 512, options [nop,nop,TS val 2329055491 ecr 2329055491], length 26
        • サーバー側から26バイトのデータが送られている。
        • シーケンス番号は、送信したバイト数分増えるので、1から開始して1+26=27まで増える。
        • パケットの中身を見ると、....Sun.Oct..9.18:31:46.2022.. というのが確認できる。
          • クライアント側の表示と一致している。
      • パケット5: IP 127.0.0.1.36162 > 127.0.0.1.13: Flags [.], ack 27, win 512, options [nop,nop,TS val 2329055491 ecr 2329055491], length 0
        • パケット4 への応答
      • パケット6: IP 127.0.0.1.13 > 127.0.0.1.36162: Flags [F.], seq 27, ack 1, win 512, options [nop,nop,TS val 2329055491 ecr 2329055491], length 0
        • サーバー側からクライアントへ FIN が通知される。
      • パケット7: IP 127.0.0.1.36162 > 127.0.0.1.13: Flags [F.], seq 1, ack 28, win 512, options [nop,nop,TS val 2329055491 ecr 2329055491], length 0
        • クライアントがサーバーの FIN に応答し ACK+FIN を返す。
      • パケット8: IP 127.0.0.1.13 > 127.0.0.1.36162: Flags [.], ack 2, win 512, options [nop,nop,TS val 2329055491 ecr 2329055491], length 0
        • サーバーが応答して、TCP終了。
18:31:46.591661 IP 127.0.0.1.36162 > 127.0.0.1.13: Flags [S], seq 1007334733, win 65495, options [mss 65495,sackOK,TS val 2329055490 ecr 0,nop,wscale 7], length 0
        0x0000:  4500 003c 3f32 4000 4006 fd87 7f00 0001  E..<?2@.@.......
        0x0010:  7f00 0001 8d42 000d 3c0a b54d 0000 0000  .....B..<..M....
        0x0020:  a002 ffd7 fe30 0000 0204 ffd7 0402 080a  .....0..........
        0x0030:  8ad2 9102 0000 0000 0103 0307            ............
18:31:46.591694 IP 127.0.0.1.13 > 127.0.0.1.36162: Flags [S.], seq 3871193739, ack 1007334734, win 65483, options [mss 65495,sackOK,TS val 2329055491 ecr 2329055490,nop,wscale 7], length 0
        0x0000:  4500 003c 0000 4000 4006 3cba 7f00 0001  E..<..@.@.<.....
        0x0010:  7f00 0001 000d 8d42 e6bd ba8b 3c0a b54e  .......B....<..N
        0x0020:  a012 ffcb fe30 0000 0204 ffd7 0402 080a  .....0..........
        0x0030:  8ad2 9103 8ad2 9102 0103 0307            ............
18:31:46.591720 IP 127.0.0.1.36162 > 127.0.0.1.13: Flags [.], ack 1, win 512, options [nop,nop,TS val 2329055491 ecr 2329055491], length 0
        0x0000:  4500 0034 3f33 4000 4006 fd8e 7f00 0001  E..4?3@.@.......
        0x0010:  7f00 0001 8d42 000d 3c0a b54e e6bd ba8c  .....B..<..N....
        0x0020:  8010 0200 fe28 0000 0101 080a 8ad2 9103  .....(..........
        0x0030:  8ad2 9103                                ....
18:31:46.592087 IP 127.0.0.1.13 > 127.0.0.1.36162: Flags [P.], seq 1:27, ack 1, win 512, options [nop,nop,TS val 2329055491 ecr 2329055491], length 26
        0x0000:  4500 004e 5584 4000 4006 e723 7f00 0001  E..NU.@.@..#....
        0x0010:  7f00 0001 000d 8d42 e6bd ba8c 3c0a b54e  .......B....<..N
        0x0020:  8018 0200 fe42 0000 0101 080a 8ad2 9103  .....B..........
        0x0030:  8ad2 9103 5375 6e20 4f63 7420 2039 2031  ....Sun.Oct..9.1
        0x0040:  383a 3331 3a34 3620 3230 3232 0d0a       8:31:46.2022..
18:31:46.592118 IP 127.0.0.1.36162 > 127.0.0.1.13: Flags [.], ack 27, win 512, options [nop,nop,TS val 2329055491 ecr 2329055491], length 0
        0x0000:  4500 0034 3f34 4000 4006 fd8d 7f00 0001  E..4?4@.@.......
        0x0010:  7f00 0001 8d42 000d 3c0a b54e e6bd baa6  .....B..<..N....
        0x0020:  8010 0200 fe28 0000 0101 080a 8ad2 9103  .....(..........
        0x0030:  8ad2 9103                                ....
18:31:46.592131 IP 127.0.0.1.13 > 127.0.0.1.36162: Flags [F.], seq 27, ack 1, win 512, options [nop,nop,TS val 2329055491 ecr 2329055491], length 0
        0x0000:  4500 0034 5585 4000 4006 e73c 7f00 0001  E..4U.@.@..<....
        0x0010:  7f00 0001 000d 8d42 e6bd baa6 3c0a b54e  .......B....<..N
        0x0020:  8011 0200 fe28 0000 0101 080a 8ad2 9103  .....(..........
        0x0030:  8ad2 9103                                ....
18:31:46.592495 IP 127.0.0.1.36162 > 127.0.0.1.13: Flags [F.], seq 1, ack 28, win 512, options [nop,nop,TS val 2329055491 ecr 2329055491], length 0
        0x0000:  4500 0034 3f35 4000 4006 fd8c 7f00 0001  E..4?5@.@.......
        0x0010:  7f00 0001 8d42 000d 3c0a b54e e6bd baa7  .....B..<..N....
        0x0020:  8011 0200 fe28 0000 0101 080a 8ad2 9103  .....(..........
        0x0030:  8ad2 9103                                ....
18:31:46.592516 IP 127.0.0.1.13 > 127.0.0.1.36162: Flags [.], ack 2, win 512, options [nop,nop,TS val 2329055491 ecr 2329055491], length 0
        0x0000:  4500 0034 5586 4000 4006 e73b 7f00 0001  E..4U.@.@..;....
        0x0010:  7f00 0001 000d 8d42 e6bd baa7 3c0a b54f  .......B....<..O
        0x0020:  8010 0200 fe28 0000 0101 080a 8ad2 9103  .....(..........
        0x0030:  8ad2 9103                                ....

  • pthread
    • POSIXスレッド。
    • スレッドの API には、SolarisスレッドとPOSIXスレッドの2つのAPIが存在する。
    • -lpthread フラグをつけてコンパイルすると、POSIXスレッドの実装にリンクされる。
    • 同時に D_REENTRANT オプションをつけてコンパイルすると、スレッドAPIを使うために必要な定数宣言が有効になる。
    • -pthread = -lpthread + -D_REENTRANT