【第2回】パケットを受信してUDPヘッダーを解析したりUDPヘッダーを設定してUDPパケットを送信するツールをC言語で作成する

本記事は「【第1回】パケットを受信してIPヘッダーを解析したりIPヘッダーを設定して送信したりするツールをC言語で作成する」の続きです。

前回はパケットを受信してIPヘッダーの内容を表示したりIPヘッダーを設定してパケットを送信しました。今回はUDPヘッダーの内容を表示したりUDPヘッダーを設定したりしてパケットを送信します。

UDPは非常にシンプルなプロトコルでヘッダー構造もシンプルなので学習には最適です。ICMPやTCPを扱う前の準備として、今回はUDPのチェックサム計算を自らおこないパケットを送信します。

本記事を読み進めるにあたり、次のスキルセットを持っていることを前提にしています。

  • IP、TCP、UDPの違いを知っている
  • C言語でのプログラミング経験がある

なお、本記事ではコードを追うことを容易にするためできるだけ関数化を避けています。またエラー処理も極力省いていますので、ご自身でコーディングされる場合は適宜エラー処理を追加したり関数化したりしてください。

UDPヘッダーフォーマット

UDPヘッダーは非常にシンプルです。送信元ポートと宛先ポート、そしてUDPヘッダーとペイロードの合計サイズ、チェックサムだけです。

 0      7 8     15 16    23 24    31
+--------+--------+--------+--------+ 
|     Source      |   Destination   | 
|      Port       |      Port       | 
+--------+--------+--------+--------+ 
|                 |                 | 
|     Length      |    Checksum     | 
+--------+--------+--------+--------+ 
|                                     
|          data octets ...            
+---------------- ...
  • Source Port(16bit):送信元ポート番号
  • DEstination Port(16bit):送信先ポート番号
  • Length(16big):UDPパケットのサイズ
  • Checksum(16bit):チェックサム
  • data octets(可変長):ペイロード

パケットを解析してIPv4ヘッダーとUDPヘッダーの値を表示する

パケットの受信と解析は「【第1回】パケットを受信してIPヘッダーを解析したりIPヘッダーを設定して送信したりするツールをC言語で作成する」とやることは同じなので簡単に説明します。

LinuxではUDPヘッダーの構造体は/usr/include/netinet/udp.hで定義されています。メンバー名の先頭に「uh_」が付くものと付かないものが定義されていますが、本記事では先頭に「uh_」が付く方を使います。

struct udphdr
{
  __extension__ union
  {
    struct
    {
      uint16_t uh_sport;        /* source port */
      uint16_t uh_dport;        /* destination port */
      uint16_t uh_ulen;         /* udp length */
      uint16_t uh_sum;          /* udp checksum */
    };
    struct
    {
      uint16_t source;
      uint16_t dest;
      uint16_t len;
      uint16_t check;
    };
  };
};

パケット受信時は第1回でも解説したようにバイトオーダーに注意しなければなりません。UDPヘッダーはすべて16bitですので、ntohs()でバイトオーダーを変換する必要があります。

バイトオーダーについては第1回の記事を参考にしてください。

IPv4パケットを受信してIPv4ヘッダーとUDPヘッダーの値(+UDPデータ)を表示するサンプルプログラム

このソースコードは第1回の記事で掲載したIPv4ヘッダー値を表示するソースコードに手を加えたものです。見るべき点は2つあります。

まずは受信したパケットがUDPなのかチェックしている点です。UDPか否かはIPヘッダーの「ip_p」の値を見れば分かります。今回はUDPなので「ip_p」が「IPPROTO_UDP」であるかチェックしています。

次にUDPヘッダーのポインタ計算です。UDPヘッダーはIPヘッダーの後ろに配置されています。つまりパケットの先頭ポインタにIPヘッダーのサイズを加算すればUDPヘッダーのポイントを求めることができます。

IPヘッダーのサイズは「ip_hl」を見れば分かります。「ip_hl」は4バイト単位でIPヘッダーのサイズを表しているので「ip_hl」の値を4倍すればIPヘッダーのサイズが分かります。以下のソースコードではIPヘッダーの先頭ポインタに「ip_hl」を4倍(左に2ビットシフト)した値を加算してUDPヘッダーの先頭ポインタを計算しています。

また、UDPデータ部分がある場合に16進数でダンプさせています。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <error.h>
#include <netinet/ip.h>
#include <netinet/udp.h>
#include <net/ethernet.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int
main(void)
{
    int sd;
    int read_len;
    int udp_datalen;
    int i;
    char buf[65535];
    struct ip *ip;
    struct udphdr *udp;
    unsigned char *p;

    if ((sd = socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL))) < 0) {
        perror("socket");
        exit(-1);
    }

    while (1)
    {
        if ((read_len = read(sd, buf, sizeof(buf))) < 0) {
            perror("read");
            exit(-1);
        }

        ip = (struct ip *)buf;
        if (ip->ip_v != 0x4)
            continue;

        printf("======== read_len: %d bytes ========\n", read_len);
        printf("ip_v: %d\n", ip->ip_v);
        printf("ip_hl: %d\n", ip->ip_hl);
        printf("ip_tos: %d\n", ip->ip_tos);
        printf("ip_len: %d\n", ntohs(ip->ip_len));
        printf("ip_id: %d\n", ntohs(ip->ip_id));
        printf("ip_off: %d\n", ntohs(ip->ip_off));
        printf("ip_ttl: %d\n", ip->ip_ttl);
        printf("ip_p: %d\n", ip->ip_p);
        printf("ip_sum: %d\n", ntohs(ip->ip_sum));
        printf("ip_src: %s\n", inet_ntoa(ip->ip_src));
        printf("ip_dst: %s\n", inet_ntoa(ip->ip_dst));

        if (ip->ip_p != IPPROTO_UDP)
            continue;

        udp = (struct udphdr *)((unsigned char *)ip + (ip->ip_hl<<2));
        printf("======== UDP length: %d bytes ========\n", ntohs(udp->uh_ulen));
        printf("uh_sport: %d\n", ntohs(udp->uh_sport));
        printf("uh_dport: %d\n", ntohs(udp->uh_dport));
        printf("uh_ulen: %d\n", ntohs(udp->uh_ulen));
        printf("uh_sum: %d\n", ntohs(udp->uh_sum));
        udp_datalen = ntohs(udp->uh_ulen) - sizeof(struct udphdr);
        if (udp_datalen > 0) {
            printf("DATA: ");
            p = (unsigned char *)udp + sizeof(struct udphdr);
            for (i = 0; i < udp_datalen; i++) {
               printf("%02x", *p++);
            }
            printf("\n");
        }
    }
}

このCコードを「linux-read_ipv4-udp_packet.c」として保存しました。このCコードはLinuxでのみコンパイル・実行できます。コンパイルする際にオプションは不要です。

cc linux-read_ipv4-udp_packet.c

実行する際はroot権限が必要になります。無限ループするので終了する際はCtrl-Cで終了してください。

$ sudo ./a.out
======== read_len: 56 bytes ========
ip_v: 4
ip_hl: 5
ip_tos: 0
ip_len: 56
ip_id: 8299
ip_off: 0
ip_ttl: 64
ip_p: 17
ip_sum: 48413
ip_src: 192.168.218.130
ip_dst: 1.1.1.1
======== UDP length: 36 bytes ========
uh_sport: 44241
uh_dport: 53
uh_ulen: 36
uh_sum: 40290
DATA: 29 7e 01 00 00 01 00 00 00 00 00 00 06 67 6f 6f 67 6c 65 03 63 6f 6d 00 00 01 00 01

この表示は1.1.1.1に対してnslookupを実行した際のものです。

UDPヘッダーを設定して送信する

第1回の記事を読んだ方であればUDPヘッダーを設定して送信するのは非常に簡単で、正直なところ説明するほどのことはありません。

そこで、本来は計算する必要がないUDPのチェックサムを計算することにしました(UDPはチェックサムの値を0にすることによってOSに計算させることができる)。これはICMPやTCPのパケット送信でも応用できます。

UDP チェックサムの計算について

チェックサムというのは、途中でパケットが壊れたり欠損していないか受信後にチェックする値です。UDPでチェックサムを計算する場合、疑似ヘッダーというものを使います。

 0      7 8     15 16    23 24    31
+--------+--------+--------+--------+
|          source address           |
+--------+--------+--------+--------+
|        destination address        |
+--------+--------+--------+--------+
|  zero  |protocol|   UDP length    |
+--------+--------+--------+--------+

この疑似ヘッダーを送信したいUDPパケットの先頭に付けてチェックサムの計算をおこないます。つまり、次のような構造になります。

 0      7 8     15 16    23 24    31
+--------+--------+--------+--------+
|          source address           |
+--------+--------+--------+--------+
|        destination address        |
+--------+--------+--------+--------+
|  zero  |protocol|   UDP length    |
+--------+--------+--------+--------+ 
|     Source      |   Destination   | 
|      Port       |      Port       | 
+--------+--------+--------+--------+ 
|                 |                 | 
|     Length      |    Checksum     | 
+--------+--------+--------+--------+ 
|                                     
|          data octets ...            
+---------------- ...

チェックサムの計算はOpenBSDのping.cに含まれる関数をそのまま使います。疑似ヘッダーとUDPヘッダーを設定して、このチェックサムの関数を通すとチェックサム値が得られます。

int
in_cksum(u_short *addr, int len)
{
	int nleft = len;
	u_short *w = addr;
	int sum = 0;
	u_short answer = 0;

	/*
	 * Our algorithm is simple, using a 32 bit accumulator (sum), we add
	 * sequential 16 bit words to it, and at the end, fold back all the
	 * carry bits from the top 16 bits into the lower 16 bits.
	 */
	while (nleft > 1) {
		sum += *w++;
		nleft -= 2;
	}

	/* mop up an odd byte, if necessary */
	if (nleft == 1) {
		*(u_char *)(&answer) = *(u_char *)w ;
		sum += answer;
	}

	/* add back carry outs from top 16 bits to low 16 bits */
	sum = (sum >> 16) + (sum & 0xffff);	/* add hi 16 to low 16 */
	sum += (sum >> 16);			/* add carry */
	answer = ~sum;				/* truncate to 16 bits */
	return(answer);
}

IPv4ヘッダーとUDPヘッダーを設定して送信する

パケットの送信は第1回の記事で解説した内容と同じくバイトオーダーに注意が必要です。今回はUDPのチェックサムを計算するため疑似ヘッダーを使っていますが、それ以外には特別なことをしていません。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/udp.h>
#include <arpa/inet.h>
#include <sys/socket.h>

/* チェックサム計算用UDP疑似ヘッダー */
struct pseudo_hdr {
    struct in_addr src;
    struct in_addr dst;
    unsigned char zero;
    unsigned char proto;
    unsigned short len;
};

int
in_cksum(u_short *addr, int len) {
    int nleft = len;
    u_short *w = addr;
    int sum = 0;
    u_short answer = 0;

    /*
     * Our algorithm is simple, using a 32 bit accumulator (sum), we add
     * sequential 16 bit words to it, and at the end, fold back all the
     * carry bits from the top 16 bits into the lower 16 bits.
     */
    while (nleft > 1) {
        sum += *w++;
        nleft -= 2;
    }

    /* mop up an odd byte, if necessary */
    if (nleft == 1) {
        *(u_char * )(&answer) = *(u_char *) w;
        sum += answer;
    }

    /* add back carry outs from top 16 bits to low 16 bits */
    sum = (sum >> 16) + (sum & 0xffff);     /* add hi 16 to low 16 */
    sum += (sum >> 16);                     /* add carry */
    answer = ~sum;                          /* truncate to 16 bits */
    return (answer);
}

int
main(int argc, char *argv[]) {
    int sd;
    int needlen;
    int sendlen;
    int on;
    int res;
    struct ip *ip;
    struct sockaddr_in src_addr;
    struct sockaddr_in dst_addr;
    struct udphdr *udp;
    struct pseudo_hdr *pse;
    char buf[65535];
    char *data;
    char *pseudo_hdr_buf;

    if (argc != 4) {
        fprintf(stderr, "Usage: %s <src ip address> <dst ip address> <data>\n", argv[0]);
        exit(1);
    }
    data = argv[3];
    /* 送信データサイズは100バイト未満にしておく */
    if (strlen(data) > 100) {
        fprintf(stderr, "data too long\n");
        exit(1);
    }

    /*
     * 送信元IPアドレスと宛先IPアドレスを設定する
     */
    if (inet_pton(AF_INET, argv[1], &src_addr.sin_addr) < 0) {
        perror("inet_pton");
        exit(1);
    }

    memset(&dst_addr, 0, sizeof(struct sockaddr_in));
    dst_addr.sin_family = AF_INET;
    if (inet_pton(AF_INET, argv[2], &dst_addr.sin_addr) < 0) {
        perror("inet_pton");
        exit(1);
    }

    if ((sd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) < 0) {
        perror("socket");
        exit(1);
    }
    /*
     * ソケットオプション
     * Linuxは設定しなくてもOK
     */
    on = 1;
    if (setsockopt(sd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0) {
        perror("setsockopt");
        exit(1);
    }

    /*
     * IPヘッダーを作成する
     */
    ip = (struct ip *) buf;
    ip->ip_v = 4;                   /* IPv4 */
    ip->ip_hl = 5;                  /* オプションがないのでヘッダーサイズは20バイト */
    ip->ip_tos = 0;                 /* TOS */
    ip->ip_len = 0;                 /* 後で計算する(Linuxはこの値を設定しなくてもOK) */
    ip->ip_id = htons(1234);        /* IPID */
    ip->ip_off = htons(0);          /* フラグメントオフセット */
    ip->ip_ttl = 64;                /* TTL */
    ip->ip_p = IPPROTO_UDP;         /* プロトコル番号 */
    ip->ip_sum = 0;                 /* チェックサムはOSが計算して設定する */
    ip->ip_src = src_addr.sin_addr; /* 送信元IPアドレス */
    ip->ip_dst = dst_addr.sin_addr; /* 宛先IPアドレス */

    /*
     * UDPヘッダー設定
     * 疑似ヘッダーの後ろに配置する
     * この時点ではチェックサムを0に設定する
     */
    udp = (struct udphdr *) (buf + (ip->ip_hl << 2));
    udp->uh_sport = htons(12345);
    udp->uh_dport = htons(56789);
    udp->uh_ulen = htons(sizeof(struct udphdr) + strlen(data));
    udp->uh_sum = 0;
    /* UDPデータ部分を書き込む */
    memcpy((unsigned char *) udp + sizeof(struct udphdr), data, strlen(data));
    
    /*
     * 疑似ヘッダーを作成する準備
     * UDPヘッダーとデータ、疑似ヘッダの合計サイズを計算し、メモリーを確保する
     */
    needlen = sizeof(struct pseudo_hdr) + ntohs(udp->uh_ulen);
    if ((pseudo_hdr_buf = malloc(needlen)) == NULL) {
        perror("malloc");
        exit(1);
    }
    memset(pseudo_hdr_buf, 0, needlen);

    /*
     * 疑似ヘッダー設定
     */
    pse = (struct pseudo_hdr *) pseudo_hdr_buf;
    pse->src = ip->ip_src;
    pse->dst = ip->ip_dst;
    pse->proto = IPPROTO_UDP;
    pse->len = udp->uh_ulen;

    /* チェックサム計算 */
    memcpy(pseudo_hdr_buf + sizeof (struct pseudo_hdr), udp, ntohs(udp->uh_ulen));
    udp->uh_sum = in_cksum((unsigned short *) pseudo_hdr_buf, needlen);
    
    /* チェックサム計算が終わり疑似ヘッダーは不要になったのでメモリーを解放する */
    free(pseudo_hdr_buf);

    /*
     * ip_len設定
     * ネットワークバイトオーダーで設定する
     * Linuxは設定しなくてもOK
     */
    sendlen = needlen - sizeof(struct pseudo_hdr) + (ip->ip_hl << 2);
    ip->ip_len = htons(sendlen);

    if ((res = sendto(sd, buf, sendlen, 0, (struct sockaddr *) &dst_addr, sizeof(dst_addr))) < 0) {
        perror("sendto");
        exit(1);
    }
    close(sd);
    printf("%d bytes sended\n", res);

    return 0;
}

このCコードを「send_ipv4-udp_packet.c」として保存しました。このCコードはLinuxと*BSDでコンパイル・実行できます。コンパイルする際にオプションは不要です。

cc send_ipv4-udp_packet.c

実行するにはroot権限が必要です。送信元IPアドレスに「1.2.3.4」を設定し、データを「ABCD」と設定して送信してみます。送信先IPアドレスは「192.168.218.133」に設定しました。これは同じLANにある別のマシンです。

$ sudo ./a.out 1.2.3.4 192.168.218.133 ABCD
32 bytes sended
$

この送信を192.168.218.133で受信してみます。最初に作成したUDP受信ツールを使ってパケットを表示させると、次のように想定通り送信されていることが確認できます。データ部分も「ABCD」を表す「41 42 43 44」と表示されています。

======== read_len: 32 bytes ========
ip_v: 4
ip_hl: 5
ip_tos: 0
ip_len: 32
ip_id: 1234
ip_off: 0
ip_ttl: 64
ip_p: 17
ip_sum: 54986
ip_src: 1.2.3.4
ip_dst: 192.168.218.133
======== UDP length: 12 bytes ========
uh_sport: 12345
uh_dport: 56789
uh_ulen: 12
uh_sum: 52751
DATA: 41 42 43 44

次に再度送信し、今度はtcpdumpでキャプチャしてみると「udp sum ok」と表示されチェックサムの計算が正しいことが分かります。そして、ふたつ目のパケットでは192.168.218.133がICMPパケットを返送しています。これはポート56789番が閉じているため、それを通知するICMP Unreachableを返送しています。

openbsd# tcpdump -i em0 -vvv -nn -t host 1.2.3.4
tcpdump: listening on em0, link-type EN10MB
1.2.3.4.12345 > 192.168.218.133.56789: [udp sum ok] udp 4 (ttl 64, id 1234, len 32)
192.168.218.133 > 1.2.3.4: icmp: 192.168.218.133 udp port 56789 unreachable [icmp cksum ok] for 1.2.3.4.12345 > 192.168.218.133.56789: udp 4 (ttl 64, id 1234, len 32) (ttl 255, id 56103, len 56)
^C
20 packets received by filter
0 packets dropped by kernel
openbsd#

チェックサムはホストバイトオーダー?ネットワークバイトオーダー?

本記事のサンプルプログラムはチェックサムをホストバイトオーダーで設定しています。チェックサムは16ビットなのでネットワークバイトオーダーで送信するのでは?という疑問があるかと思います。

結論から書くと、チェックサムはホストバイトオーダーで設定します。試しにチェックサムをネットワークバイトオーダーに設定して送信してみましょう。以下のようにチェックサムの設定をネットワークバイトオーダーに変更します。

udp->uh_sum = htons(in_cksum((unsigned short *) pseudo_hdr_buf, needlen));

コンパイルして先ほどと同様に送信し受信側でtcpdumpを実行してみると、チェックサムの値が間違っていることが分かります。「bad udp cksum ce0c! -> 0cce」とあるように、ce0cではなくて0cceが正しいチェックサム値です。

openbsd# tcpdump -i em0 -vvv -nn -t host 1.2.3.4
tcpdump: listening on em0, link-type EN10MB
1.2.3.4.12345 > 192.168.218.133.56789: [bad udp cksum ce0c! -> 0cce] udp 4 (ttl 64, id 1234, len 32)
^C
1254 packets received by filter
0 packets dropped by kernel
openbsd#

まとめ

今回は意図的にチェックサム計算を取り入れたため少しコードが長くなりましたが、UDPについてはチェックサム計算は不要なのでご自身で実装される際は省略しても問題ないかと思います。

ただし、今後ICMPやTCPでヘッダーを設定して送信する場合にはチェックサムの計算が必須となるので、シンプルなUDPで慣れておくと今後の開発で役に立つはずです。

この記事は役に立ちましたか?

もし参考になりましたら、下記のボタンで教えてください。

関連記事

コメント

この記事へのコメントはありません。