Help us understand the problem. What is going on with this article?

パケットを自前で生成してTCP接続する

More than 3 years have passed since last update.

目的

諸般の事情により、RAW_SOCKを開いてL4パケットを流さないといけないことになってしまったので、その方法について簡単にまとめた。

OSI参照モデルとヘッダの構造

TCPはOSI参照モデル第4層(いわゆるL4)のプロトコルである。
このレイヤで見た場合のパケットの構造は以下の様になる。

Ethernet Header (Layer2)
IP Header (Layer3)
TCP Header (layer4)
TCP Data

以下のセクションではパケットの構成方法について簡単に説明する。

パケット全体の構成

今回はTCP接続の事始めとなる、three-way handshakeのsynパケットを発生させるところまで頑張る。
ヘッダはL2-L4まで必要で、これを一連のバイト列として構成する必要がある。
そのため、まず、大きめのバイト列を作成しておき、そこからL2,L3,L4の順で切り出していく。

以下の設定例のセクションは下記が設定されている前提とする。

example.c
char packet[1500];
memset(buf, 0, sizeof(buf));

L2

定義(抜粋)

net/ethernet.h
struct ether_header
{
  u_int8_t  ether_dhost[ETH_ALEN];  /* destination eth addr */
  u_int8_t  ether_shost[ETH_ALEN];  /* source ether addr    */
  u_int16_t ether_type;             /* packet type ID field */
} __attribute__ ((__packed__));

変数名 設定の意味
ether_dhost[ETH_ALEN] 宛先ホストのMACアドレスを16進数で1桁ずつ指定する。
ether_shost[ETH_ALEN] 送信元ホストのMACアドレスを16進数で1桁ずつ指定する。
ether_type パケットの種類を書く。例えばIP,ARPなど。詳細はnet/ethernet.h内のEthernet protocol ID'sセクションに記載されている。

設定例

ether_header *eh;
eh = (struct ether_header *)packet;

eh->ether_shost[0] = 0x08
eh->ether_shost[2] = 0x00
...
eh->ether_shost[6] = 0xEB

eh->ether_dhost[0] = 0x08
eh->ether_dhost[2] = 0x00
...
eh->ether_dhost[6] = 0xE3

eh->ether_type=htons(ETHERTYPE_IP);

L3

定義(抜粋)

netinet/ip.h
struct iphdr
  {
#if __BYTE_ORDER == __LITTLE_ENDIAN
    unsigned int ihl:4;
    unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
    unsigned int version:4;
    unsigned int ihl:4;
#else
# error "Please fix <bits/endian.h>"
#endif
    u_int8_t tos;
    u_int16_t tot_len;
    u_int16_t id;
    u_int16_t frag_off;
    u_int8_t ttl;
    u_int8_t protocol;
    u_int16_t check;
    u_int32_t saddr;
    u_int32_t daddr;
    /*The options start here. */
  };

正直今回使っていないものは大して調べていない。

変数名 設定の意味
version IPプロトコルのバージョン。今のところ4か6。
ihl IPヘッダの長さを4倍と単位で指定する。最小はオプションを持たない場合の20バイトとなり、20/4=5を指定する。
tos サービス品質を指定。
tot_len IPヘッダ以降にあるパケットの全体長。
id データを分割した場合にデータを識別するための値。
frag_off 分割された場合に、先頭から何バイト目で切り取られたか。
ttl パケットが何回ルータに中継されたか。0になると破棄される。
protocol データのプロトコル。例えばTCPやUDP、ICMPなどがある。詳細はnetinet/ip.h内に定義されている。
check パケットのチェックサム(後述)
saddr 送信元IPアドレス
daddr 宛先IPアドレス

checksumの算出

ヘッダの内容に対して、16bit単位で1の補数和を取り、さらにその1の補数を取る。
ちなみに、参考1からの抜粋なので、詳しくはそちらを参照。

u_int16_t checksum(u_char *data,int len)
{
register u_int32_t  sum;
register u_int16_t  *ptr;
register int        c;

        sum=0;
        ptr=(u_int16_t *)data;

        for(c=len;c>1;c-=2){
                sum+=(*ptr);
                if(sum&0x80000000){
                        sum=(sum&0xFFFF)+(sum>>16);
                }
                ptr++;
        }
        if(c==1){
                u_int16_t       val;
                val=0;
                memcpy(&val,ptr,sizeof(u_int8_t));
                sum+=val;
        }

        while(sum>>16){
                sum=(sum&0xFFFF)+(sum>>16);
        }

    return(~sum);
}

以降の例でチェックサムを算出するときは上記関数を利用する。

設定例

iphdr *ih;
ih = (struct iphdr *)(packet+sizeof(struct ether_header));

ih->version=4;
ih->ihl=20/4;
ih->tos=0;
ih->tot_len=htons(sizeof(struct tcphdr)+sizeof(struct iphdr));
ih->id=0;
ih->frag_off=0;
ih->ttl=64;
ih->protocol=IPPROTO_TCP;
ih->check=0;
inet_aton("192.168.0.1",(struct in_addr *)&ih->saddr);
inet_aton("192.168.0.10",(struct in_addr *)&ih->daddr);
ih->check=checksum((u_char *)ih,sizeof(struct iphdr));

L4

定義(抜粋)

netinet/tcp.h
struct tcphdr
  {
    u_int16_t source;
    u_int16_t dest;
    u_int32_t seq;
    u_int32_t ack_seq;
#  if __BYTE_ORDER == __LITTLE_ENDIAN
    u_int16_t res1:4;
    u_int16_t doff:4;
    u_int16_t fin:1;
    u_int16_t syn:1;
    u_int16_t rst:1;
    u_int16_t psh:1;
    u_int16_t ack:1;
    u_int16_t urg:1;
    u_int16_t res2:2;
#  elif __BYTE_ORDER == __BIG_ENDIAN
    u_int16_t doff:4;
    u_int16_t res1:4;
    u_int16_t res2:2;
    u_int16_t urg:1;
    u_int16_t ack:1;
    u_int16_t psh:1;
    u_int16_t rst:1;
    u_int16_t syn:1;
    u_int16_t fin:1;
#  else
#   error "Adjust your <bits/endian.h> defines"
#  endif
    u_int16_t window;
    u_int16_t check;
    u_int16_t urg_ptr;
};
変数名 設定の意味
source 送信元ポート番号
dest 宛先ポート番号
ack_seq ackシーケンス番号
doff このヘッダの先頭から見て、TCPデータが始まる位置。4byte単位で数えるので、最小値は5。
res1 予約
res2 予約
urg 緊急データが含まれる場合は1。ただしほとんど使われないらしい。
ack ACKフラグがONの場合は1。
psh pshフラグがONの場合は1。
rst rstフラグがONの場合は1。
syn synフラグがONの場合は1。
fin finフラグがONの場合は1。
window Windowサイズ
check チェックサム
urg_ptr 緊急データの場所を表す数値。よくわからない。

設定例

TCPの場合はチェックサムの算出が若干ややこしく、この正式なTCPヘッダの前に、擬似IPヘッダを付けて算出する必要がある。
この実現方法については、参考2をかなり借用している。

// 擬似パケットの構造体を定義。
struct pseudoTCPPacket {
        uint32_t        srcAddr;
        uint32_t        dstAddr;
        uint8_t         zero;
        uint8_t         protocol;
        uint16_t        TCP_len;
};

...

struct pseudoTCPPacket pTCPPacket;
char *pseudo_packet;

tcphdr *th;
th = (struct tcphdr *)(packet+sizeof(struct ether_header)+sizeof(struct iphdr));

th->source=htons(49963);
th->dest=htons(80);
th->ack_seq=0;
th->doff=5;
th->urg=0;
th->ack=0;
th->psh=0;
th->rst=0;
th->syn=1;
th->fin=0;

th->window=500;
th->urg_ptr=0;

// 擬似パケットの作成
pseudo_packet = (char *)malloc((int) (sizeof(struct pseudoTCPPacket)+sizeof(struct tcphdr)));
memset(pseudo_packet, 0, sizeof(struct pseudoTCPPacket)+sizeof(struct tcphdr));

pTCPPacket.srcAddr=inet_addr("192.168.0.1");
pTCPPacket.dstAddr=inet_addr("192.168.0.10");
pTCPPacket.zero=0;
pTCPPacket.protocol=IPPROTO_TCP;
pTCPPacket.TCP_len=htons(sizeof(struct tcphdr));

memcpy(pseudo_packet, (char *)&pTCPPacket, sizeof(struct pseudoTCPPacket));
memcpy(pseudo_packet+sizeof(struct pseudoTCPPacket), th, sizeof(struct tcphdr));

// チェックサムの算出
th->check=(checksum((u_char *)pseudo_packet, (int) (sizeof(struct pseudoTCPPacket)+sizeof(struct tcphdr))));

パケットの送出

これは非常に単純で、開いたソケットに作成したバイト列を書き込めば良い。

これからについて

戻ってきたSYN+ACKのパケットにACKを返して、その後RSTで接続を落とすところまで作成したい。
最終的な目的はコネクション数ベースの負荷試験なので、その目的が果たせるようなものを作成する。

あと、このバイト列を書き込んだところからのLinux的な動作も調べてまとめておきたい。

個人的な感想

C言語は初学の頃にちょこっと触っただけで、実に10年ぶりくらいにまともに触った。
当時はこういうプログラムを書かないまま、JavaとかPythonみたいな充実した言語に移行してしまい、なんとなくC言語は面倒で大変なだけの言語だと思っていたが、今回触ってみてchar *を構造体にキャストすることで、うまくパケットのバイト列を操作できるものだと感心した。

構造体が宣言された順にメモリ空間に配置されていくということを認識していなかったので、一瞬戸惑った。

こういう低レイヤのプログラミングにおいては、C言語が記法的に優れていると感じた。

参考

  1. ルーター自作でわかるパケットの流れ
  2. raw_tcp_socket
marufeuille
エンジニアに返り咲いた
gsoftbank
Alibaba Cloud(アリババクラウド)の日本向けサービスのローカライズや日本語サポートを行っているほか、AI、ビッグデータ、IoT技術などを活用したシステム構築サービスを提供しています。
https://www.sbcloud.co.jp/archive/category/techblog
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away