TFO(tcp fastopen)で遊んでいて気付いたんですが、sendto(2)とsendmsg(2)はflagsに未知の値をセットしたときの扱いが両者で挙動の差があるっぽい。
このflags引数はMSG_PEEKとかMSG_XXXなものを論理和とった結果を渡しますが、この変数に未知の値を渡したとき(今はTFOなので0x20000000を渡したとき)にsendmsg(2)は-EINVALを返してくるけどsendto(2)はエラーにならずに処理が成功しました。
ちなみに、TFOの値を0x20000000としているのはlinuxカーネルのヘッダー(include/linux/socket.h)でMSG_FASTOPENがそのように定義されているためです。
ユーザーランドでは/usr/include/bits/socket.hにMSG_PEEK等の値が定義されています。fedoraの場合はこのヘッダはglibc-headersパッケージに含まれているのですが、glibc-headers-2.16-31.fc18.x86_64ではMSG_FASTOPENが定義されていないので自前で0x20000000を渡す必要があります。
なんでこんなこと調べていたかというと、pythonはsendto、rubyはsendmsgがサポートされているのですが、pythonは上手くいくのにrubyだとダメだったからですね。
ということでテストコードを書いて確認ですが、クライアントはsendto(2)とsendmsg(2)を両方試すためにcで。
まずはサーバはこんな感じで。
#!/usr/bin/env ruby require "socket" def start_server(port = 1111) sock = Socket.open(Socket::AF_INET, Socket::SOCK_STREAM, 0) # Defined in include/linux/tcp.h # #define TCP_FASTOPEN 23 /* Enable FastOpen on listeners */ sock.setsockopt(Socket::SOL_TCP, 23, 5) sock.bind(Addrinfo.tcp("127.0.0.1", port)) sock.listen(5) while true s = sock.accept req = s[0].recv(64, Socket::MSG_PEEK) s[0].printf "fuction is %s\n", req s[0].close end sock.close end if __FILE__ == $0 then start_server() end
挙動確認用のテストクライアントはcで。
TFOを使う関数(tfo_sendtoとtfo_sendmsg)はconnect(2)は使わずにsocketを作ったらsendto(2)/sendmsg(2)を実行できますが、tfo無しのtfo_sendmsgではconnect(2)が必要です。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <errno.h> #define SERVER_ADDR "127.0.0.1" #define SERVER_PORT 1111 // Basically MSG_XXX are, such as MSG_PEEK, defined in /usr/include/bits/socket.h // but glibc-headers-2.16-31.fc18.x86_64 does not define it. #define MSG_FASTOPNE_VALUE 0x20000000 static int tfo_sendto(int sock); static int tfo_sendmsg(int sock); static int notfo_sendmsg(int sock); typedef int (*send_data_func_t)(int sock); static void error_exit(void) { fprintf(stderr, "[-]Error: %s\n", strerror(errno)); exit(-1); } #define set_sockaddr_in(sin) \ do { \ sin.sin_family = AF_INET; \ sin.sin_addr.s_addr = htonl(INADDR_ANY); \ sin.sin_port = htons(SERVER_PORT); \ } while (0) static int tfo_sendto(int sock) { char buf[16] = "sendo"; struct sockaddr_in sin = { 0 }; socklen_t slen = sizeof(sin); printf("[-]Start %s\n", __FUNCTION__); set_sockaddr_in(sin); return sendto(sock, buf, strlen(buf), MSG_FASTOPNE_VALUE, (struct sockaddr *) &sin, slen); } #define set_msghdr(msg, iov, buf) \ do { \ iov[0].iov_base = buf; \ iov[0].iov_len = strlen(buf); \ msg.msg_iov = iov; \ msg.msg_iovlen = (sizeof(iov) / sizeof(iov[0])); \ } while (0) static int notfo_sendmsg(int sock) { struct iovec iov[1]; struct msghdr msg = { 0 }; struct sockaddr_in sin = { 0 }; socklen_t slen = sizeof(sin); char buf[16] = "notfo sendmsg"; printf("[-]Start %s\n", __FUNCTION__); set_msghdr(msg, iov, buf); set_sockaddr_in(sin); connect(sock, (struct sockaddr *) &sin, slen); return sendmsg(sock, &msg, 0); } static int tfo_sendmsg(int sock) { struct iovec iov[1]; struct msghdr msg = { 0 }; char buf[16] = "tfo sendmsg"; printf("[-]Start %s\n", __FUNCTION__); set_msghdr(msg, iov, buf); return sendmsg(sock, &msg, MSG_FASTOPNE_VALUE); } static void tfo_test_start(send_data_func_t func) { int sock = 0; int ret = 0; char buf[32] = { 0 }; printf("[-]Start %s\n", __FUNCTION__); sock = socket(AF_INET, SOCK_STREAM, 0); if (sock == -1) error_exit(); ret = func(sock); if (ret < 0) error_exit(); ret = recv(sock, buf, sizeof(buf), MSG_PEEK); if (ret == -1) error_exit(); printf("[-]Receve data is \n"); printf(" %s", buf); } int main(int argc, char **argv) { send_data_func_t func = &tfo_sendto; if (argc == 2) { if (!strcmp("sendmsg", argv[1])) func = &tfo_sendmsg; else if (!strcmp("notfo_sendmsg", argv[1])) func = ¬fo_sendmsg; } tfo_test_start(func); return 0; }
これをコンパイルして実行すると以下のようになります。
[masami@saga:~]$ gcc msg_fastopen_test.c -g -Wall [masami@saga:~]$ ./a.out [-]Start tfo_test_start [-]Start tfo_sendto [-]Receve data is fuction is sendo [masami@saga:~]$ ./a.out notfo_sendmsg [-]Start tfo_test_start [-]Start notfo_sendmsg [-]Receve data is fuction is notfo sendmsg [masami@saga:~]$ ./a.out sendmsg [-]Start tfo_test_start [-]Start tfo_sendmsg [-]Error: Invalid argument
と、このようにTFOのsendmesgの場合にInvalid argumentで失敗します。
straceを使って何が起きているか見てみます。まずはsendto(2)の場合。
write(1, "[-]Start tfo_sendto\\n", 20[-]Start tfo_sendto) = 20 sendto(3, "sendo", 5, 0x20000000 /* MSG_??? */, {sa_family=AF_INET, sin_port=htons(1111), sin_addr=inet_addr("0.0.0.0")}, 16) = 5 recvfrom(3, "fuction is sendo\\n", 32, MSG_PEEK, NULL, NULL) = 17
0x20000000に対して「/* MSG_??? */」何てでてますねー。まあglibc的には未知の値ですし・・・というのはありますが関数自体は失敗しないんですよね。
では、sendmsg(2)の場合、
write(1, "[-]Start tfo_sendmsg\\n", 21[-]Start tfo_sendmsg ) = 21 sendmsg(3, {msg_name(0)=NULL, msg_iov(1)=[{"tfo sendmsg", 11}], msg_controllen=0, msg_flags=0}, 0x20000000 /* MSG_??? */) = -1 EINVAL (Invalid argument) write(2, "[-]Error: Invalid argument\\n", 27[-]Error: Invalid argument) = 27 exit_group(-1) = ?
こちらも0x20000000に対して「/* MSG_??? */」が出てますが、sendmsg(2)の場合はEINVAL (Invalid argument)でエラーになってしまいますね。
この結果から、sendto(2)はflags変数に未知の値がセットされていてもエラーにならずにデータをそのまま使う、sendmsg(2)の場合はエラーにするということが分かりました。
このチェックをしているのがglibcなのかカーネルなのかは調べてませんが。
今回のまとめは、カーネルでc言語を使うのは当たり前なので特に気にしてなかったけど、ユーザランドでc言語を使うのは面倒ですね><
あ、TFOをrubyでやろうと思ってsendmsgの使い方を調べたので自分のメモとしてコード貼り付け。
#!/usr/bin/env ruby require "socket" def connect_server(server = "127.0.0.1", port = 1111) sock = Socket.open(Socket::AF_INET, Socket::SOCK_STREAM, 0) addr = Socket.sockaddr_in(port, server) #sock.connect(addr) ancdata = Socket::AncillaryData.new(Socket::AF_INET, Socket::SOL_SOCKET, Socket::SCM_RIGHTS, "SCM_RIGHTS") sock.sendmsg("TFO", 0x20000000, nil, ancdata) #sock.sendmsg("foobar", 0, nil, ancdata) res = sock.recv(16, Socket::MSG_PEEK) printf "%s", res sock.close end if __FILE__ == $0 then connect_server() end