本記事はライブラリに頼らず自力で低レイヤーのネットワークプログラミングをおこなう場合に注意するべき点をまとめました。
Linuxでパケットの送受信をおこなうプログラミング方法は以下の記事で解説しており、本記事は以下の記事の補足となっています。
- 【第1回】パケットを受信してIPヘッダーを解析したりIPヘッダーを設定して送信したりするツールをC言語で作成する
- 【第2回】パケットを受信してUDPヘッダーを解析したりUDPヘッダーを設定してUDPパケットを送信するツールをC言語で作成する
- 【第3回】パケットを受信してTCPヘッダーを解析するツールをC言語で作成する
- 【第4回】TCPヘッダーを設定して送信するツールをC言語で作成する
目次
バイトオーダーの取り扱い
LinuxはIPヘッダーの値をすべてネットワークバイトオーダーで設定します。それに対して*BSDではフラグメントオフセットとIP長の取り扱いが異なります。
わたしが調べたところ2024年1月現在では以下の状態となっています。
FreeBSD 13.1
- IP長:ネットワークバイトオーダー
- フラグメントオフセット:ネットワークバイトオーダー
OpenBSD 7.4
- IP長:ネットワークバイトオーダー
- フラグメントオフセット:ネットワークバイトオーダー
NetBSD 9.3
- IP長:ホストバイトオーダー
- フラグメントオフセット:ホストバイトオーダー
昔は*BSDのIP長とフラグメントオフセットはホストバイトオーダーで設定することが定石だったのですが、現在ではNetBSDのみIP長とフラグメントオフセットをホストバイトオーダーで設定する必要があります。IPヘッダーを独自に実装する際は注意が必要です。
ソケットオプション
IPv4プロトコルをユーザー空間で実装するにはrawソケットを開く必要があります(別名:生ソケット)。rawソケットを開くとリンクレベルレイヤのヘッダーを含まないrawデータグラムの送信が可能となります。
Linuxの仕様
Linuxではrawソケットを開く際のプロトコルにIPPROTO_RAWを指定すると自動的にIP_HDRINCLオプションが有効化されます。このオプションが有効化されると、あらゆるIPプロトコルを送信できるようになります。
IP_HDRINCLオプションが有効化されている場合、IPヘッダーの値は次のように設定されます。
- チェックサム:OSが計算して自動的に設定する
- 送信元IPアドレス:0の場合、自動的に設定する
- IPID:0の場合、自動的に設定する
- IP長:sendto(2)で指定する送信サイズにOSが自動的に設定する
BSDの仕様
BSDはrawソケットを開く際のプロトコルにIPPROTO_RAWを指定しても自動的にIP_HDRINCLオプションが有効化されることはありません。そのため、BSDでは明示的にソケットオプションを設定する必要があります。
IP_HDRINCLオプションが有効化されている場合、IPヘッダーの値は次のように設定されます。
- チェックサム:OSが計算して自動的に設定する
- 送信元IPアドレス:0の場合、自動的に設定する
- IPID:0の場合、自動的に設定する
BSDはIP長が自動的に設定されないので注意が必要です。
IP_HDRINCLオプションを有効化せずに送信するとどうなるのか?
第1回の記事で掲載したパケット送信のCコードを再掲します。このCコードを使ってテストしてみましょう。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <sys/socket.h> int main(int argc, char *argv[]) { int sd; int on; struct ip *ip; struct sockaddr_in src_addr; struct sockaddr_in dst_addr; char buf[256]; if (argc != 3) { fprintf(stderr, "Usage: %s <src ip address> <dst ip address>\n", argv[0]); 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 = htons(sizeof(struct ip)); /* Linuxはこの値を設定しなくてもOK */ ip->ip_id = htons(1234); /* IPID */ ip->ip_off = htons(0); /* フラグメントオフセット */ ip->ip_ttl = 64; /* TTL */ ip->ip_p = IPPROTO_RAW; /* プロトコル番号 */ ip->ip_sum = 0; /* チェックサムはOSが計算して設定する */ ip->ip_src = src_addr.sin_addr; /* 送信元IPアドレス */ ip->ip_dst = dst_addr.sin_addr; /* 宛先IPアドレス */ if (sendto(sd, buf, sizeof(struct ip), 0, (struct sockaddr *)&dst_addr, sizeof(dst_addr)) < 0) { perror("sendto"); exit(1); } close(sd); return 0; }
ソケットオプションを設定しているのは以下の箇所です。
/* * ソケットオプション * Linuxは設定しなくてもOK */ on = 1; if (setsockopt(sd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0) { perror("setsockopt"); exit(1); }
on = 1
をon = 0
にするとIP_HDRINCLオプションが無効化されます。
/* * ソケットオプション * Linuxは設定しなくてもOK */ on = 0; if (setsockopt(sd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0) { perror("setsockopt"); exit(1); }
次のようにコメントアウトするとソケットオプションの設定自体をおこないません。
/* * ソケットオプション * Linuxは設定しなくてもOK */ #if 0 on = 1; if (setsockopt(sd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0) { perror("setsockopt"); exit(1); } #endif
それではLinuxとOpenBSDを使って以下の3パターンで結果を見てみます。
- IP_HDRINCLオプションを有効化する
- IP_HDRINCLオプションを無効化する
- IP_HDRINCLオプションを設定しない
Linuxの場合
IP_HDRINCLオプションを有効化する
IP_HDRINCLオプションが有効化された状態で送信すると、次のように送信元IPアドレスが1.2.3.4となります。
1.2.3.4 > 192.168.218.133: ip-proto-255 0 (ttl 64, id 1234, len 20)
IP_HDRINCLオプションを無効化する
IP_HDRINCLオプションを無効化すると送信元IPアドレスがLinuxマシンの192.168.218.130となりました。また、idも7948となっており、設定した1234ではありません。
IP_HDRINCLオプションを無効化すると設定したIPヘッダーを含めずに送信することが分かりました。
192.168.218.130 > 192.168.218.133: ip-proto-255 20 (DF) (ttl 64, id 7948, len 40)
IP_HDRINCLオプションを設定しない
IP_HDRINCLオプションを設定しない場合、このように送信先IPアドレスが1.2.3.4となっており、idも1234になっているので設定したIPヘッダーが送信されていることが分かります。
1.2.3.4 > 192.168.218.133: ip-proto-255 0 (ttl 64, id 1234, len 20)
*BSDの場合
IP_HDRINCLオプションを有効化する
IP_HDRINCLオプションが有効化された状態で送信すると、次のように送信元IPアドレスが1.2.3.4となります。idも設定した1234になっています。
ens33 In IP (tos 0x0, ttl 64, id 1234, offset 0, flags [none], proto unknown (255), length 20) 1.2.3.4 > 192.168.218.130: ip-proto-255 0
IP_HDRINCLオプションを無効化する
IP_HDRINCLオプションを無効化すると送信元IPアドレスがOpenBSDマシンの192.168.218.133となりました。また、idも61606となっており、設定した1234ではありません。
Linuxと同様にIP_HDRINCLオプションを無効化すると設定したIPヘッダーを含めずに送信することが分かりました。
ens33 In IP (tos 0x0, ttl 255, id 61606, offset 0, flags [none], proto unknown (255), length 40) 192.168.218.133 > 192.168.218.130: ip-proto-255 20
IP_HDRINCLオプションを設定しない
IP_HDRINCLオプションを設定しない場合、IP_HDRINCLオプションを無効化したときと同様に送信元IPアドレスがOpenBSDマシンの192.168.218.133となりました。また、idも49768となっており、設定した1234ではありません。
ens33 In IP (tos 0x0, ttl 255, id 49768, offset 0, flags [none], proto unknown (255), length 40) 192.168.218.133 > 192.168.218.130: ip-proto-255 20
このように、*BSDの場合はIPヘッダーを含めて送信する際にIP_HDRINCLオプションを有効化しなければなりません。
IP長を誤って設定すると、どうなるのか?
ふたたび以下のCコードを使います。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <sys/socket.h> int main(int argc, char *argv[]) { int sd; int on; struct ip *ip; struct sockaddr_in src_addr; struct sockaddr_in dst_addr; char buf[256]; if (argc != 3) { fprintf(stderr, "Usage: %s <src ip address> <dst ip address>\n", argv[0]); 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 = htons(sizeof(struct ip)); /* Linuxはこの値を設定しなくてもOK */ ip->ip_id = htons(1234); /* IPID */ ip->ip_off = htons(0); /* フラグメントオフセット */ ip->ip_ttl = 64; /* TTL */ ip->ip_p = IPPROTO_RAW; /* プロトコル番号 */ ip->ip_sum = 0; /* チェックサムはOSが計算して設定する */ ip->ip_src = src_addr.sin_addr; /* 送信元IPアドレス */ ip->ip_dst = dst_addr.sin_addr; /* 宛先IPアドレス */ if (sendto(sd, buf, sizeof(struct ip), 0, (struct sockaddr *)&dst_addr, sizeof(dst_addr)) < 0) { perror("sendto"); exit(1); } close(sd); return 0; }
この中でIP長を設定しているのは以下の箇所です。
ip->ip_len = htons(sizeof(struct ip)); /* Linuxはこの値を設定しなくてもOK */
この箇所を以下の2パターンの値に設定したとき、どのような状態になるのでしょうか?試してみましょう。
- ip_lenを0に設定する
- ip_lenをでたらめな値(12345)に設定する
Linuxの場合
ip_lenを0に設定する
Linuxではip_lenを0に設定しても送信時にエラーが発生しません。
linux$ sudo ./a.out 1.2.3.4 192.168.218.133 linux$
受信側でも以下のようにlenの値が20になっていることが確認できるので、正常に受信しています。
1.2.3.4 > 192.168.218.133: ip-proto-255 0 (ttl 64, id 1234, len 20)
ip_lenをでたらめな値(12345)に設定する
ip_lenにでたらめな値を設定しても、Linuxでは送信時にエラーが発生しません。
linux$ sudo ./a.out 1.2.3.4 192.168.218.133 linux$
受信側でも以下のようにlenの値が20になっていることが確認できるので、正常に受信しています。
1.2.3.4 > 192.168.218.133: ip-proto-255 0 (ttl 64, id 1234, len 20)
このように、Linuxではip_lenの値を設定してもしなくても、誤った値を設定しても結果は変わりませんでした。
*BSDの場合
ip_lenを0に設定する
OpenBSDではip_lenに0を設定すると送信に失敗します。
openbsd# ./a.out 1.2.3.4 192.168.218.130 sendto: Invalid argument openbsd#
ip_lenをでたらめな値(12345)に設定する
でたらめな値をip_lenに設定した場合も送信に失敗します。
openbsd# ./a.out 1.2.3.4 192.168.218.130 sendto: Invalid argument openbsd#
このように、*BSDではIP長(ip_len)とsendto(2)で指定する送信先サイズが一致していない場合はエラーとなり送信できませんでした。
まとめ
昔はLinuxと*BSDでのバイトオーダーの取り扱いだけ注意していれば良かったのですが、現在は*BSDでもNetBSDだけ過去と変わらずバイトオーダーの取り扱いが異なります。
このあたりは将来的に解決される可能性がありますが、IPヘッダーを設定して送信するような実装をされる際は注意が必要となります。
この記事は役に立ちましたか?
もし参考になりましたら、下記のボタンで教えてください。
コメント