目的
諸般の事情により、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の順で切り出していく。
以下の設定例のセクションは下記が設定されている前提とする。
char packet[1500];
memset(buf, 0, sizeof(buf));
L2
定義(抜粋)
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
定義(抜粋)
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
定義(抜粋)
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言語が記法的に優れていると感じた。