Linux カーネルパラメータに net.core.somaxconn というものがあり、それと一緒に設定することが多いパラメータに net.ipv4.tcp_max_syn_backlog というものがある。

そういえば backlog って何なんだろう…となったのでメモ。

backlog とは何か

listen システムコールの引数には backlog というものがある。

int listen(int sockfd, int backlog);

listen(2) によって作られたソケットは backlog というキューを持つことになる。
会社で @hiboma さんに聞いたところ、listen queue と呼ばれることもあるらしい。
これは netstat で出力される(後述)ところから来ているのかなーとのこと。

backlog には次の2種類がある。

では、カーネルのソースコードを追いながら、次のことを確認していく。

ちなみに環境は次の通り。

$ uname -a
Linux ubuntu-bionic 4.15.0-62-generic #69-Ubuntu SMP Wed Sep 4 20:55:53 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

listen(2) におけるキューの初期化

まずは listen システムコールから見ていく。

listen(2) で作成されるキューがいわゆる backlog(listen queue) と呼ばれるもので、ここでは Listen Queue と表す。
対して、後述するが、 accept(2) によって作られる backlog を Accept Queue と呼ぶことにする。

Linux において、システムコールは SYSCALL_DEFINE2 で定義されるので、 SYSCALL_DEFINE2(listen などで検索する。

SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
	struct socket *sock;
	int err, fput_needed;
	int somaxconn;

	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (sock) {
		somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
		if ((unsigned int)backlog > somaxconn)
			backlog = somaxconn;

		err = security_socket_listen(sock, backlog);
		if (!err)
			err = sock->ops->listen(sock, backlog);

		fput_light(sock->file, fput_needed);
	}
	return err;
}

net.ipv4.tcp_max_syn_backlognet.core.somaxconn の値より大きい場合は net.core.somaxconn の値を優先するのだが、その処理はこのあたりっぽい。

		somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
		if ((unsigned int)backlog > somaxconn)
			backlog = somaxconn;

err = sock->ops->listen(sock, backlog); のところで inet_listen が呼ばれる。

int inet_listen(struct socket *sock, int backlog)
{
	struct sock *sk = sock->sk;
	unsigned char old_state;
	int err, tcp_fastopen;
  ...
 	/* Really, if the socket is already in listen state
	 * we can only allow the backlog to be adjusted.
	 */
	if (old_state != TCP_LISTEN) {
		/* Enable TFO w/o requiring TCP_FASTOPEN socket option.
		 * Note that only TCP sockets (SOCK_STREAM) will reach here.
		 * Also fastopen backlog may already been set via the option
		 * because the socket was in TCP_LISTEN state previously but
		 * was shutdown() rather than close().
		 */
		tcp_fastopen = sock_net(sk)->ipv4.sysctl_tcp_fastopen;
		if ((tcp_fastopen & TFO_SERVER_WO_SOCKOPT1) &&
		    (tcp_fastopen & TFO_SERVER_ENABLE) &&
		    !inet_csk(sk)->icsk_accept_queue.fastopenq.max_qlen) {
			fastopen_queue_tune(sk, backlog);
			tcp_fastopen_init_key_once(sock_net(sk));
		}

		err = inet_csk_listen_start(sk, backlog);
		if (err)
			goto out;
	}
	sk->sk_max_ack_backlog = backlog;
	err = 0;
  ...

さらに inet_csk_listen_start が呼ばれている。

int inet_csk_listen_start(struct sock *sk, int backlog)
{
	struct inet_connection_sock *icsk = inet_csk(sk);
	struct inet_sock *inet = inet_sk(sk);
	int err = -EADDRINUSE;

	reqsk_queue_alloc(&icsk->icsk_accept_queue);

	sk->sk_max_ack_backlog = backlog;
	sk->sk_ack_backlog = 0;
	inet_csk_delack_init(sk);

	/* There is race window here: we announce ourselves listening,
	 * but this transition is still not validated by get_port().
	 * It is OK, because this socket enters to hash table only
	 * after validation is complete.
	 */
	sk_state_store(sk, TCP_LISTEN);
	if (!sk->sk_prot->get_port(sk, inet->inet_num)) {
		inet->inet_sport = htons(inet->inet_num);

		sk_dst_reset(sk);
		err = sk->sk_prot->hash(sk);

		if (likely(!err))
			return 0;
	}

	sk->sk_state = TCP_CLOSE;
	return err;
}

ここでは reqsk_queue_alloc() というキューを作成するっぽい関数が呼ばれている。

void reqsk_queue_alloc(struct request_sock_queue *queue)
{
	spin_lock_init(&queue->rskq_lock);

	spin_lock_init(&queue->fastopenq.lock);
	queue->fastopenq.rskq_rst_head = NULL;
	queue->fastopenq.rskq_rst_tail = NULL;
	queue->fastopenq.qlen = 0;

	queue->rskq_accept_head = NULL;
}

コメントにある通り、キューの数は128が最小値らしい。

inet_csk_listen_start() に戻り、backlog の最大値が設定されている。

	sk->sk_max_ack_backlog = backlog;
	sk->sk_ack_backlog = 0;

ここまでで Listen Queue のサイズは backlog で指定した値のサイズになることが確認できた。

では SYN パケットを受け取る処理を見ていく。
3-Way Handshake 時、カーネルではどのように遷移しているのかを表した図として、 https://www.tldp.org/HOWTO/KernelAnalysis-HOWTO-8.html が参考になる。

*** 3-Way TCP Handshake ***
|switch(sk->state)
|case TCP_LISTEN: // We received SYN
   |conn_request -> tcp_v4_conn_request
      |tcp_v4_send_synack // Send SYN + ACK
         |tcp_v4_synq_add // set SYN state
|case TCP_SYN_SENT: // we received SYN + ACK
   |tcp_rcv_synsent_state_process
      tcp_set_state(TCP_ESTABLISHED)
         |tcp_send_ack
            |tcp_transmit_skb
               |queue_xmit -> ip_queue_xmit
                  |ip_queue_xmit2
                     |skb->dst->output
|case TCP_SYN_RECV: // We received ACK
   |if (ACK)
      |tcp_set_state(TCP_ESTABLISHED)

tcp_v4_conn_request の中では tcp_conn_request が呼ばれる。

int tcp_conn_request(struct request_sock_ops *rsk_ops,
		     const struct tcp_request_sock_ops *af_ops,
		     struct sock *sk, struct sk_buff *skb)
{
        ...
        /* TW buckets are converted to open requests without
      	 * limitations, they conserve resources and peer is
      	 * evidently real one.
      	 */
      	if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
      	     inet_csk_reqsk_queue_is_full(sk)) && !isn) {
      		want_cookie = tcp_syn_flood_action(sk, skb, rsk_ops->slab_name);
      		if (!want_cookie)
      			goto drop;
      	}
      
      	if (sk_acceptq_is_full(sk)) {
      		NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
      		goto drop;
      	}
      
      	req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);
      	if (!req)
      		goto drop;
      
      	tcp_rsk(req)->af_specific = af_ops;
      	tcp_rsk(req)->ts_off = 0;
        ...
}

ちょっとずつ追っていく。

      	if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
      	     inet_csk_reqsk_queue_is_full(sk)) && !isn) {
      		want_cookie = tcp_syn_flood_action(sk, skb, rsk_ops->slab_name);

inet_csk_reqsk_queue_is_full() は次のようになっている。
この関数では Listen Queue がいっぱいになっていないかをチェックしている。

static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk)
{
	return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}

ここで net->ipv4.sysctl_tcp_syncookies == 2tcp_syn_flood_action() 関数などがでてきたので、そちらも確認しよう。

これは SYN Flood 対策のためのもので、Listen Queue いっぱいにキューが溜まった場合、新規にキューに突っ込むことができずにプロセスと疎通が取れなくなってしまう。
そこで SYN Cookies と呼ばれるものが作られた。
それを有効/無効にするのが net.ipv4.tcp_syncookies だ。

tcp_syn_flood_action() は次のようになっている。

static bool tcp_syn_flood_action(const struct sock *sk,
				 const struct sk_buff *skb,
				 const char *proto)
{
	struct request_sock_queue *queue = &inet_csk(sk)->icsk_accept_queue;
	const char *msg = "Dropping request";
	bool want_cookie = false;
	struct net *net = sock_net(sk);

#ifdef CONFIG_SYN_COOKIES
	if (net->ipv4.sysctl_tcp_syncookies) {
		msg = "Sending cookies";
		want_cookie = true;
		__NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPREQQFULLDOCOOKIES);
	} else
#endif
		__NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPREQQFULLDROP);

	if (!queue->synflood_warned &&
	    net->ipv4.sysctl_tcp_syncookies != 2 &&
	    xchg(&queue->synflood_warned, 1) == 0)
		pr_info("%s: Possible SYN flooding on port %d. %s.  Check SNMP counters.\n",
			proto, ntohs(tcp_hdr(skb)->dest), msg);

	return want_cookie;
}

net.ipv4.tcp_syncookies の値が0の場合はそのまま破棄され、1の場合は syn cookie 付きの SYN/ACK を返している。

というわけで次のことがわかった。

tcp_conn_request に戻って次の処理へ。
Accept Queue がいっぱいの場合は先程同様 DROP する。

	if (sk_acceptq_is_full(sk)) {
		NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
		goto drop;
	}
static inline bool sk_acceptq_is_full(const struct sock *sk)
{
	return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}

ここを Pass するとSYN/ACK を送信して Accept Queue に加える。

		af_ops->send_synack(fastopen_sk, dst, &fl, req,
				    &foc, TCP_SYNACK_FASTOPEN);
		/* Add the child socket directly into the accept queue */
		inet_csk_reqsk_queue_add(sk, req, fastopen_sk);

ちなみに drop は次のような処理になっている。

drop:
	tcp_listendrop(sk);
	return 0;
static inline void tcp_listendrop(const struct sock *sk)
{
	atomic_inc(&((struct sock *)sk)->sk_drops);
	__NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENDROPS);
}

netstat でいう TcpExtListenDrops を増加させるらしい。

ここまでで、次のような流れを確認できた。

  1. SYN を受け取ると listen queue へ格納する
  2. SYN/ACK を返すと同時に accept queue へ移動する

現在、クライアントは SYN_SENT, サーバーは SYN_RECV の状態にある。
では、クライアントから ACK を受け取ったときの処理を追いかけていく。

tcp_v4_do_recv() をみていく。

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
	struct sock *rsk;

	if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
		struct dst_entry *dst = sk->sk_rx_dst;

		sock_rps_save_rxhash(sk, skb);
		sk_mark_napi_id(sk, skb);
		if (dst) {
			if (inet_sk(sk)->rx_dst_ifindex != skb->skb_iif ||
			    !dst->ops->check(dst, 0)) {
				dst_release(dst);
				sk->sk_rx_dst = NULL;
			}
		}
		tcp_rcv_established(sk, skb, tcp_hdr(skb));
		return 0;
	}

	if (tcp_checksum_complete(skb))
		goto csum_err;

	if (sk->sk_state == TCP_LISTEN) {
		struct sock *nsk = tcp_v4_cookie_check(sk, skb);

		if (!nsk)
			goto discard;
		if (nsk != sk) {
			if (tcp_child_process(sk, nsk, skb)) {
				rsk = nsk;
				goto reset;
			}
			return 0;
		}
	} else
		sock_rps_save_rxhash(sk, skb);

	if (tcp_rcv_state_process(sk, skb)) {
		rsk = sk;
		goto reset;
	}
	return 0;

tcp_rcv_state_process() へ。

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
  ...
	if (req) {
		WARN_ON_ONCE(sk->sk_state != TCP_SYN_RECV &&
		    sk->sk_state != TCP_FIN_WAIT1);

		if (!tcp_check_req(sk, skb, req, true))
			goto discard;
	}
  ...

tcp_check_req() へ。

struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
			   struct request_sock *req,
			   bool fastopen)
{
	/* OK, ACK is valid, create big socket and
	 * feed this segment to it. It will repeat all
	 * the tests. THIS SEGMENT MUST MOVE SOCKET TO
	 * ESTABLISHED STATE. If it will be dropped after
	 * socket is created, wait for troubles.
	 */
	child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL,
							 req, &own_req);
	if (!child)
		goto listen_overflow;

	sock_rps_save_rxhash(child, skb);
	tcp_synack_rtt_meas(child, req);
	return inet_csk_complete_hashdance(sk, child, req, own_req);
  ...

ここでも tcp_v4_syn_recv_sock()->sk_acceptq_is_full() が呼ばれている。

struct sock *tcp_v4_syn_recv_sock(const struct sock *sk, struct sk_buff *skb,
				  struct request_sock *req,
				  struct dst_entry *dst,
				  struct request_sock *req_unhash,
				  bool *own_req)
{
	struct inet_request_sock *ireq;
	struct inet_sock *newinet;
	struct tcp_sock *newtp;
	struct sock *newsk;
#ifdef CONFIG_TCP_MD5SIG
	struct tcp_md5sig_key *key;
#endif
	struct ip_options_rcu *inet_opt;

	if (sk_acceptq_is_full(sk))
		goto exit_overflow;

キューがいっぱいの場合は exit_overflow へ移るようだ。

exit_overflow:
	NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);

これは netstat で確認できる ListenOverflows の値を増加させている。
ここの定義を見ると netstat -s の意味が理解できて便利。

最終的に呼び出し元の tcp_check_req() では listen_overflow へ処理が移る。

listen_overflow:
	if (!sock_net(sk)->ipv4.sysctl_tcp_abort_on_overflow) {
		inet_rsk(req)->acked = 1;
		return NULL;
	}

これは net.ipv4.tcp_abort_on_overflow=1 の場合に RST を送るという処理になる。
もし 0 であれば、何もしない。

では次に ACK を受け取って以降の処理、つまり、 accpet(2) の処理を見ていく。

accept(2)

inet_csk_accept() 関数を見ていく。

struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
{
	struct inet_connection_sock *icsk = inet_csk(sk);
	struct request_sock_queue *queue = &icsk->icsk_accept_queue;
	struct request_sock *req;
	struct sock *newsk;
	int error;

	lock_sock(sk);

	/* We need to make sure that this socket is listening,
	 * and that it has something pending.
	 */
	error = -EINVAL;
	if (sk->sk_state != TCP_LISTEN)
		goto out_err;

	/* Find already established connection */
	if (reqsk_queue_empty(queue)) {
		long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);

		/* If this is a non blocking socket don't sleep */
		error = -EAGAIN;
		if (!timeo)
			goto out_err;

		error = inet_csk_wait_for_connect(sk, timeo);
		if (error)
			goto out_err;
	}
	req = reqsk_queue_remove(queue, sk);
	newsk = req->sk;

reqsk_queue_remove() でキューから削除している。

ここまでのまとめ

backlog の数以上の接続があった場合

では backlog 以上の接続が発生した場合どうなるのだろうか。
netcat は backlog=1 のため、これで確認してみる。

$ strace nc -lvp 12345
...
bind(3, {sa_family=AF_INET, sin_port=htons(12345), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 1)                            = 0
write(2, "Listening on [0.0.0.0] (family 0"..., 46Listening on [0.0.0.0] (family 0, port 12345)
) = 46
...

ss -tan で確認すると Send-Q が 1 になっている。ここは backlog の数を示すらしい。

$ ss -tan
State        Recv-Q        Send-Q                Local Address:Port                 Peer Address:Port
LISTEN       0             128                         0.0.0.0:22                        0.0.0.0:*
LISTEN       0             5                         127.0.0.1:631                       0.0.0.0:*
LISTEN       0             128                         0.0.0.0:8088                      0.0.0.0:*
LISTEN       0             1                           0.0.0.0:12345                     0.0.0.0:*

10 並列でつないでみる。

$ for i in `seq 10`; do echo '12345'; done | xargs -t -P 10 -n 1 nc localhost

ss -tan の結果、Recv-Q は2となっている。

$ ss -tan | grep 12345
LISTEN    2        1                   0.0.0.0:12345            0.0.0.0:*

sk_acceptq_is_full() は次のような条件式のため、backlog + 1 だけキューを持てることになり、Recv-Q は 2 になっている。

static inline bool sk_acceptq_is_full(const struct sock *sk)
{
	return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}

そのため、キューが2なので10並列で接続しても Recv-Q はこれ以上増えない。

パケットのドロップを確認する

キューがいっぱいの時は LINUX_MIB_LISTENOVERFLOWSLINUX_MIB_LISTENDROPS が増加することが分かっているので、それを確認してみる。

現在のカウントは91となっている。

$ nstat -az TcpExtListenDrops
#kernel
TcpExtListenDrops               91                 0.0
$ nstat -az TcpExtListenOverflows
#kernel
TcpExtListenOverflows           91                 0.0

ここで先程同様に backlog=1 なプロセスに対して複数の接続を行う。
すると119に増加していることが確認できた。

$ nstat -az TcpExtListenOverflows
#kernel
TcpExtListenOverflows           119                0.0
$ nstat -az TcpExtListenDrops
#kernel
TcpExtListenDrops               119                0.0

ESTABLISHED なクライアントの数だけ再送されて、この数がその分増加される。
tcpdump で確認すると確かに SYN パケットが送信されているが、レスポンスはない。

    localhost.46684 > localhost.12345: Flags [S], cksum 0xfe30 (incorrect -> 0x4251), seq 3634709843, win 43690, options [mss 65495,sackOK,TS val 2368131368 ecr 0,nop,wscale 7], length 0
12:20:12.260146 IP (tos 0x0, ttl 64, id 46559, offset 0, flags [DF], proto TCP (6), length 60)
    localhost.46678 > localhost.12345: Flags [S], cksum 0xfe30 (incorrect -> 0xfe00), seq 3475804962, win 43690, options [mss 65495,sackOK,TS val 2368131368 ecr 0,nop,wscale 7], length 0
12:20:12.260150 IP (tos 0x0, ttl 64, id 40247, offset 0, flags [DF], proto TCP (6), length 60)
    localhost.46674 > localhost.12345: Flags [S], cksum 0xfe30 (incorrect -> 0x9eb9), seq 97958852, win 43690, options [mss 65495,sackOK,TS val 2368131368 ecr 0,nop,wscale 7], length 0

Ref