φ(・・*)ゞ ウーン sendto(2)とsendmsg(2)に渡すflagsに関する挙動の差

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 = &notfo_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