Byeo

Listen socket의 TCP SYN 처리 1 본문

프로그래밍 (Programming)/네트워크 스택

Listen socket의 TCP SYN 처리 1

BKlee 2024. 5. 25. 20:36
반응형

accept system call은 established된 connection에 대해서 fd를 할당해 userspace에게 제공합니다.

 

그러면 3-way handshake의 TCP_SYN은 누가 처리할까요? 한 번 그 과정을 알아봅니다.

 

단, tcp code만 살펴볼 예정이고, device driver와 ip layer는 스킵합니다. 나중에 read system call에서 깊이 다루게 될 것 같습니다.

 

★ 이 로직은 application에 의해서 trigger되는 로직이 아닙니다! NIC이 packet을 수신하여 interrupt가 발생했고, device driver가 이를 처리하기 위해 trigger되는 로직(으로 예상을 하고있고, 나중에 read에서 확인할 예정)입니다!

 

1. tcp_v4_rcv

tcp_v4_rcv라는 함수는 인자로 sk_buff* skb를 받아서 tcp 수신 처리를 진행합니다. 이 함수에서 sk->sk_state가 TCP_LISTEN인지를 체크하는 코드가 있습니다.

 

// net/ipv4/tcp_ipv4.c/tcp_v4_rcv() :2093

	if (sk->sk_state == TCP_LISTEN) { printk("[byeo: tcp_v4_rcv] sk ptr %p\n", sk->sk_socket);
		ret = tcp_v4_do_rcv(sk, skb);
		goto put_and_return;
	}

 

이 if문을 타는지 한 번 체크하기 위해서 printk를 넣어보았습니다.

 

socket() 당시 할당받았던 struct sock*과 tcp_v4_rcv에서 찍은 struct sock*의 pointer가 같으므로 같은 socket이라고 볼 수 있겠군요. 결국은 저 if branch를 탄다는 것을 확인했습니다.

 

  •  Linex kernel에서 fd를 struct file으로 바꾸는 함수는 있으나, struct file을 fd로 바꾸는 함수는 찾지 못했습니다. 만약 그런 함수가 있다면 sk->sk_socket->file 을 fd로 바꿔서 비교해볼텐데 말이죠.. 혹시 좋은 방법이 있다면 공유부탁드립니다~

 

2. tcp_v4_do_rcv

// net/ipv4/tcp_ipv4.c/tcp_v4_do_rcv() :1696

/* The socket must have it's spinlock held when we get
 * here, unless it is a TCP_LISTEN socket.
 *
 * We have a potential double-lock case here, so even when
 * doing backlog processing we use the BH locking scheme.
 * This is because we cannot sleep with the original spinlock
 * held.
 */
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 ||
			    !INDIRECT_CALL_1(dst->ops->check, ipv4_dst_check,
					     dst, 0)) {
				dst_release(dst);
				sk->sk_rx_dst = NULL;
			}
		}
		tcp_rcv_established(sk, 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;

reset:
	tcp_v4_send_reset(rsk, skb);
discard:
	kfree_skb(skb);
	/* Be careful here. If this function gets more complicated and
	 * gcc suffers from register pressure on the x86, sk (in %ebx)
	 * might be destroyed here. This current version compiles correctly,
	 * but you have been warned.
	 */
	return 0;

csum_err:
	trace_tcp_bad_csum(skb);
	TCP_INC_STATS(sock_net(sk), TCP_MIB_CSUMERRORS);
	TCP_INC_STATS(sock_net(sk), TCP_MIB_INERRS);
	goto discard;
}
EXPORT_SYMBOL(tcp_v4_do_rcv);

 

tcp_v4_do_rcv 함수입니다. sk->sk_state == TCP_ESTABLISHED는 건너 뛰겠습니다.

 

	if (tcp_checksum_complete(skb))
		goto csum_err;

 

체크섬을 검사하는 항목입니다. skb의 구조체를 확인해 checksum 연산이 필요한지 확인하고, 필요하면 checksum 검사를 실행합니다.

 

 

2-1. tcp_v4_cookie_check

// net/ipv4/tcp_ipv4.c/tcp_v4_cookie_check() :1660

static struct sock *tcp_v4_cookie_check(struct sock *sk, struct sk_buff *skb)
{
#ifdef CONFIG_SYN_COOKIES
	const struct tcphdr *th = tcp_hdr(skb);

	if (!th->syn)
		sk = cookie_v4_check(sk, skb);
#endif
	return sk;
}

 

TCP SYN_COOKIE와 관련이 있는 함수입니다. syn cookie는 ddos 공격인 syn flooding을 방지하기 위해서 추가된 기능인데요. SYN-ACK의 #seq에 magic number를 넣어서 (1)  SYN queue를 제거하면서도 (2) SYN이 왔었음을 간주하는 방법입니다.

 여기서는 3-way handshake에서 client가 보낸 3rd ACK packet의 ack 값이 올바른지를 확인합니다. 자세한 내용은 넘기겠습니다.

 

SYN_COOKIES관련 로직은 스킵하겠습니다.

 

 

이 현재 SYN packet이므로 sk를 return할 것이고, nsk와 sk가 같으므로 1726 줄 (tcp_child_process)은 실행하지 않습니다.

 

3. tcp_rcv_state_process

그러면 tcp_v4_do_rcv가 다음으로 실행하는 함수는 tcp_rcv_state_process입니다.

 

// net/ipv4/tcp_input.c/tcp_fcv_state_process() :6354

/*
 *	This function implements the receiving procedure of RFC 793 for
 *	all states except ESTABLISHED and TIME_WAIT.
 *	It's called from both tcp_v4_rcv and tcp_v6_rcv and should be
 *	address independent.
 */

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk);
	const struct tcphdr *th = tcp_hdr(skb);
	struct request_sock *req;
	int queued = 0;
	bool acceptable;

	switch (sk->sk_state) {
	case TCP_CLOSE:
		goto discard;

	case TCP_LISTEN:
		if (th->ack)
			return 1;

		if (th->rst)
			goto discard;

		if (th->syn) {
			if (th->fin)
				goto discard;
			/* It is possible that we process SYN packets from backlog,
			 * so we need to make sure to disable BH and RCU right there.
			 */
			rcu_read_lock();
			local_bh_disable();
			acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0;
			local_bh_enable();
			rcu_read_unlock();

			if (!acceptable)
				return 1;
			consume_skb(skb);
			return 0;
		}
		goto discard;

	case TCP_SYN_SENT:
		tp->rx_opt.saw_tstamp = 0;
		tcp_mstamp_refresh(tp);
		queued = tcp_rcv_synsent_state_process(sk, skb, th);
		if (queued >= 0)
			return queued;

		/* Do step6 onward by hand. */
		tcp_urg(sk, skb, th);
		__kfree_skb(skb);
		tcp_data_snd_check(sk);
		return 0;
	}

	tcp_mstamp_refresh(tp);
	tp->rx_opt.saw_tstamp = 0;
	req = rcu_dereference_protected(tp->fastopen_rsk,
					lockdep_sock_is_held(sk));
	if (req) {
		bool req_stolen;

		WARN_ON_ONCE(sk->sk_state != TCP_SYN_RECV &&
		    sk->sk_state != TCP_FIN_WAIT1);

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

	if (!th->ack && !th->rst && !th->syn)
		goto discard;

	if (!tcp_validate_incoming(sk, skb, th, 0))
		return 0;

	/* step 5: check the ACK field */
	acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH |
				      FLAG_UPDATE_TS_RECENT |
				      FLAG_NO_CHALLENGE_ACK) > 0;

	if (!acceptable) {
		if (sk->sk_state == TCP_SYN_RECV)
			return 1;	/* send one RST */
		tcp_send_challenge_ack(sk, skb);
		goto discard;
	}
	switch (sk->sk_state) {
	case TCP_SYN_RECV:
		tp->delivered++; /* SYN-ACK delivery isn't tracked in tcp_ack */
		if (!tp->srtt_us)
			tcp_synack_rtt_meas(sk, req);

		if (req) {
			tcp_rcv_synrecv_state_fastopen(sk);
		} else {
			tcp_try_undo_spurious_syn(sk);
			tp->retrans_stamp = 0;
			tcp_init_transfer(sk, BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB,
					  skb);
			WRITE_ONCE(tp->copied_seq, tp->rcv_nxt);
		}
		smp_mb();
		tcp_set_state(sk, TCP_ESTABLISHED);
		sk->sk_state_change(sk);

		/* Note, that this wakeup is only for marginal crossed SYN case.
		 * Passively open sockets are not waked up, because
		 * sk->sk_sleep == NULL and sk->sk_socket == NULL.
		 */
		if (sk->sk_socket)
			sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);

		tp->snd_una = TCP_SKB_CB(skb)->ack_seq;
		tp->snd_wnd = ntohs(th->window) << tp->rx_opt.snd_wscale;
		tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);

		if (tp->rx_opt.tstamp_ok)
			tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;

		if (!inet_csk(sk)->icsk_ca_ops->cong_control)
			tcp_update_pacing_rate(sk);

		/* Prevent spurious tcp_cwnd_restart() on first data packet */
		tp->lsndtime = tcp_jiffies32;

		tcp_initialize_rcv_mss(sk);
		tcp_fast_path_on(tp);
		break;

	case TCP_FIN_WAIT1: {
		int tmo;

		if (req)
			tcp_rcv_synrecv_state_fastopen(sk);

		if (tp->snd_una != tp->write_seq)
			break;

		tcp_set_state(sk, TCP_FIN_WAIT2);
		sk->sk_shutdown |= SEND_SHUTDOWN;

		sk_dst_confirm(sk);

		if (!sock_flag(sk, SOCK_DEAD)) {
			/* Wake up lingering close() */
			sk->sk_state_change(sk);
			break;
		}

		if (tp->linger2 < 0) {
			tcp_done(sk);
			NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
			return 1;
		}
		if (TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq &&
		    after(TCP_SKB_CB(skb)->end_seq - th->fin, tp->rcv_nxt)) {
			/* Receive out of order FIN after close() */
			if (tp->syn_fastopen && th->fin)
				tcp_fastopen_active_disable(sk);
			tcp_done(sk);
			NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
			return 1;
		}

		tmo = tcp_fin_time(sk);
		if (tmo > TCP_TIMEWAIT_LEN) {
			inet_csk_reset_keepalive_timer(sk, tmo - TCP_TIMEWAIT_LEN);
		} else if (th->fin || sock_owned_by_user(sk)) {
			/* Bad case. We could lose such FIN otherwise.
			 * It is not a big problem, but it looks confusing
			 * and not so rare event. We still can lose it now,
			 * if it spins in bh_lock_sock(), but it is really
			 * marginal case.
			 */
			inet_csk_reset_keepalive_timer(sk, tmo);
		} else {
			tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
			goto discard;
		}
		break;
	}

	case TCP_CLOSING:
		if (tp->snd_una == tp->write_seq) {
			tcp_time_wait(sk, TCP_TIME_WAIT, 0);
			goto discard;
		}
		break;

	case TCP_LAST_ACK:
		if (tp->snd_una == tp->write_seq) {
			tcp_update_metrics(sk);
			tcp_done(sk);
			goto discard;
		}
		break;
	}

	/* step 6: check the URG bit */
	tcp_urg(sk, skb, th);

	/* step 7: process the segment text */
	switch (sk->sk_state) {
	case TCP_CLOSE_WAIT:
	case TCP_CLOSING:
	case TCP_LAST_ACK:
		if (!before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) {
			/* If a subflow has been reset, the packet should not
			 * continue to be processed, drop the packet.
			 */
			if (sk_is_mptcp(sk) && !mptcp_incoming_options(sk, skb))
				goto discard;
			break;
		}
		fallthrough;
	case TCP_FIN_WAIT1:
	case TCP_FIN_WAIT2:
		/* RFC 793 says to queue data in these states,
		 * RFC 1122 says we MUST send a reset.
		 * BSD 4.4 also does reset.
		 */
		if (sk->sk_shutdown & RCV_SHUTDOWN) {
			if (TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq &&
			    after(TCP_SKB_CB(skb)->end_seq - th->fin, tp->rcv_nxt)) {
				NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
				tcp_reset(sk, skb);
				return 1;
			}
		}
		fallthrough;
	case TCP_ESTABLISHED:
		tcp_data_queue(sk, skb);
		queued = 1;
		break;
	}

	/* tcp_data could move socket to TIME-WAIT */
	if (sk->sk_state != TCP_CLOSE) {
		tcp_data_snd_check(sk);
		tcp_ack_snd_check(sk);
	}

	if (!queued) {
discard:
		tcp_drop(sk, skb);
	}
	return 0;
}
EXPORT_SYMBOL(tcp_rcv_state_process);

 

 

길지만 볼 내용은 사실 짧은 함수입니다.

 

	case TCP_LISTEN:
		if (th->ack)
			return 1;

		if (th->rst)
			goto discard;

		if (th->syn) {
			if (th->fin)
				goto discard;
			/* It is possible that we process SYN packets from backlog,
			 * so we need to make sure to disable BH and RCU right there.
			 */
			rcu_read_lock();
			local_bh_disable();
			acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0;
			local_bh_enable();
			rcu_read_unlock();

			if (!acceptable)
				return 1;
			consume_skb(skb);
			return 0;
		}
		goto discard;

 

th->ack라면 1을 return하여  tcp_v4_do_rcv의 tcp_v4_send_reset을 호출하게 합니다. 즉, rst packet을 보낼 것으로 보입니다. LISTEN상태인데 ack가 먼저 오는 것은 말이 안되죠.

 

th->rst라면 discard label로 이동해서 tcp_drop을 호출합니다. tcp_drop은 sk->drop 바이트를 증가시키고 (통계목적으로 사용할 것으로 추측됩니다.) skb_free를 실행합니다.

 

th->syn이고 th->fin이 아니라면 icsk->icsk_af_ops->conn_request(sk, skb)를 실행합니다.

 

socket() 에 의해서 tcp_v4_init_sock이 호출된 적이 있고, 그에 따라 icsk->icsk_af_ops는 ipv4_specific으로 지정되었었습니다.

 

// net/ipv4/tcp_ipv4.c:2209

const struct inet_connection_sock_af_ops ipv4_specific = {
	.queue_xmit	   = ip_queue_xmit,
	.send_check	   = tcp_v4_send_check,
	.rebuild_header	   = inet_sk_rebuild_header,
	.sk_rx_dst_set	   = inet_sk_rx_dst_set,
	.conn_request	   = tcp_v4_conn_request,
	.syn_recv_sock	   = tcp_v4_syn_recv_sock,
	.net_header_len	   = sizeof(struct iphdr),
	.setsockopt	   = ip_setsockopt,
	.getsockopt	   = ip_getsockopt,
	.addr2sockaddr	   = inet_csk_addr2sockaddr,
	.sockaddr_len	   = sizeof(struct sockaddr_in),
	.mtu_reduced	   = tcp_v4_mtu_reduced,
};
EXPORT_SYMBOL(ipv4_specific);

 

 

conn_request는 tcp_v4_conn_request함수로 지정이 되어있네요!

 

 

4. tcp_v4_conn_request

// net/ipv4/tcp_ipv4.c/tcp_v4_conn_request() :1519

int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
	/* Never answer to SYNs send to broadcast or multicast */
	if (skb_rtable(skb)->rt_flags & (RTCF_BROADCAST | RTCF_MULTICAST))
		goto drop;

	return tcp_conn_request(&tcp_request_sock_ops,
				&tcp_request_sock_ipv4_ops, sk, skb);

drop:
	tcp_listendrop(sk);
	return 0;
}

 

첫 if문은 주석에 적혀있듯, broadcast 및 multicast packet에는 응답을 하지 않도록 하는 코드입니다.

 

바로 tcp_conn_request를 호출합니다.

 

5. tcp_conn_request

// net/ipv4/tcp_input.c/tcp_conn_request() :6804

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)
{
	struct tcp_fastopen_cookie foc = { .len = -1 };
	__u32 isn = TCP_SKB_CB(skb)->tcp_tw_isn;
	struct tcp_options_received tmp_opt;
	struct tcp_sock *tp = tcp_sk(sk);
	struct net *net = sock_net(sk);
	struct sock *fastopen_sk = NULL;
	struct request_sock *req;
	bool want_cookie = false;
	struct dst_entry *dst;
	struct flowi fl;

	/* 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, 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;

	req->syncookie = want_cookie;
	tcp_rsk(req)->af_specific = af_ops;
	tcp_rsk(req)->ts_off = 0;
#if IS_ENABLED(CONFIG_MPTCP)
	tcp_rsk(req)->is_mptcp = 0;
#endif

	tcp_clear_options(&tmp_opt);
	tmp_opt.mss_clamp = af_ops->mss_clamp;
	tmp_opt.user_mss  = tp->rx_opt.user_mss;
	tcp_parse_options(sock_net(sk), skb, &tmp_opt, 0,
			  want_cookie ? NULL : &foc);

	if (want_cookie && !tmp_opt.saw_tstamp)
		tcp_clear_options(&tmp_opt);

	if (IS_ENABLED(CONFIG_SMC) && want_cookie)
		tmp_opt.smc_ok = 0;

	tmp_opt.tstamp_ok = tmp_opt.saw_tstamp;
	tcp_openreq_init(req, &tmp_opt, skb, sk);
	inet_rsk(req)->no_srccheck = inet_sk(sk)->transparent;

	/* Note: tcp_v6_init_req() might override ir_iif for link locals */
	inet_rsk(req)->ir_iif = inet_request_bound_dev_if(sk, skb);

	dst = af_ops->route_req(sk, skb, &fl, req);
	if (!dst)
		goto drop_and_free;

	if (tmp_opt.tstamp_ok)
		tcp_rsk(req)->ts_off = af_ops->init_ts_off(net, skb);

	if (!want_cookie && !isn) {
		/* Kill the following clause, if you dislike this way. */
		if (!net->ipv4.sysctl_tcp_syncookies &&
		    (net->ipv4.sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
		     (net->ipv4.sysctl_max_syn_backlog >> 2)) &&
		    !tcp_peer_is_proven(req, dst)) {
			/* Without syncookies last quarter of
			 * backlog is filled with destinations,
			 * proven to be alive.
			 * It means that we continue to communicate
			 * to destinations, already remembered
			 * to the moment of synflood.
			 */
			pr_drop_req(req, ntohs(tcp_hdr(skb)->source),
				    rsk_ops->family);
			goto drop_and_release;
		}

		isn = af_ops->init_seq(skb);
	}

	tcp_ecn_create_request(req, skb, sk, dst);

	if (want_cookie) {
		isn = cookie_init_sequence(af_ops, sk, skb, &req->mss);
		if (!tmp_opt.tstamp_ok)
			inet_rsk(req)->ecn_ok = 0;
	}

	tcp_rsk(req)->snt_isn = isn;
	tcp_rsk(req)->txhash = net_tx_rndhash();
	tcp_rsk(req)->syn_tos = TCP_SKB_CB(skb)->ip_dsfield;
	tcp_openreq_init_rwin(req, sk, dst);
	sk_rx_queue_set(req_to_sk(req), skb);
	if (!want_cookie) {
		tcp_reqsk_record_syn(sk, req, skb);
		fastopen_sk = tcp_try_fastopen(sk, skb, req, &foc, dst);
	}
	if (fastopen_sk) {
		af_ops->send_synack(fastopen_sk, dst, &fl, req,
				    &foc, TCP_SYNACK_FASTOPEN, skb);
		/* Add the child socket directly into the accept queue */
		if (!inet_csk_reqsk_queue_add(sk, req, fastopen_sk)) {
			reqsk_fastopen_remove(fastopen_sk, req, false);
			bh_unlock_sock(fastopen_sk);
			sock_put(fastopen_sk);
			goto drop_and_free;
		}
		sk->sk_data_ready(sk);
		bh_unlock_sock(fastopen_sk);
		sock_put(fastopen_sk);
	} else {
		tcp_rsk(req)->tfo_listener = false;
		if (!want_cookie)
			inet_csk_reqsk_queue_hash_add(sk, req,
				tcp_timeout_init((struct sock *)req));
		af_ops->send_synack(sk, dst, &fl, req, &foc,
				    !want_cookie ? TCP_SYNACK_NORMAL :
						   TCP_SYNACK_COOKIE,
				    skb);
		if (want_cookie) {
			reqsk_free(req);
			return 0;
		}
	}
	reqsk_put(req);
	return 0;

drop_and_release:
	dst_release(dst);
drop_and_free:
	__reqsk_free(req);
drop:
	tcp_listendrop(sk);
	return 0;
}
EXPORT_SYMBOL(tcp_conn_request);

 

 

함수를 한 번 훑어보면 알겠지만 명시적으로 skc->state를 TCP_SYN_RECV로 변경하는 로직은 없습니다. tcp diagram을 보면 SYN을 수신 시, TCP_SYN_RECV로 변경된다고 그려져 있습니다. 하지만 실제로 tcp_set_state함수를 전체 검색해보면 TCP_SYN_RECV로 변경하는 경우는 tcp simultaneous open말고 없었습니다.

  • 대신에, SYN packet을 수신하면 reqsk를 새로 할당할 때 TCP_NEW_SYN_RECV state로 관리하며, listen socket의 SYN queue에 보관합니다. 이들을 SYN_RECV라고 볼 수 있겠습니다.

혹시나 코드 해석에 문제가 있는 것은 아니었을까 해서 검색해본 결과 몇 가지 자료를 찾을 수 있었습니다. [각주:1] [각주:2]

 

 

5-1. inet_csk_reqsk_queue_is_full

// net/ipv4/tcp_input.c/tcp_conn_request() :6823

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

 

 syn_cookie 설정값과 accept_queue가 꽉 찼는지 확인합니다. 

  • sysctl_tcp_syncookies의 설정값에 대한 의미는 다른 블로그에서 잘 정리해두었습니다. [각주:3]
  • sysctl에서 tcp_syncookie가 2인 경우에는 accept queue (reqsk)의 상태와 관계없이 syncookie를 발행합니다.
  • sysctl에서 tcp_syncookie가 1인 경우에는 accept queue가 꽉찼을 때에만 syncookie를 발행합니다.

아래는 inet_csk_reqsk_queue_is_full 함수입니다.

// include/net/inet_connection_sock.h :275

static inline int inet_csk_reqsk_queue_len(const struct sock *sk)
{
	return reqsk_queue_len(&inet_csk(sk)->icsk_accept_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;
}

 

 

5-2. sk_acceptq_is_full

// net/ipv4/tcp_input.c/tcp_conn_request() :6830

	if (sk_acceptq_is_full(sk)) {
		NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
		goto drop;
	}

 

// include/net/sock.h/sk_acceptq_is_full() :948

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

 

sk->ack_backlog가 꽉 찼다면 (listen 당시 주어진 backlog 값을 넘겼다면), 통계를 처리하고 drop처리합니다.

 

5-3. inet_reqsk_alloc

// net/ipv4/tcp_input.c/tcp_conn_request() :6835

	req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);
	if (!req)
		goto drop;

 

reqsk를 할당합니다. 해당 함수는 다음과같이 정의되어 있습니다. syn cookie가 아닌 상황을 가정할 것이므로, want_cookie는 false고 !want_cookie는 true라고 가정합니다.

 

// net/ipv4/tcp_input.c/inet_reqsk_alloc() :6691

struct request_sock *inet_reqsk_alloc(const struct request_sock_ops *ops,
				      struct sock *sk_listener,
				      bool attach_listener)
{
	struct request_sock *req = reqsk_alloc(ops, sk_listener,
					       attach_listener);

	if (req) {
		struct inet_request_sock *ireq = inet_rsk(req);

		ireq->ireq_opt = NULL;
#if IS_ENABLED(CONFIG_IPV6)
		ireq->pktopts = NULL;
#endif
		atomic64_set(&ireq->ir_cookie, 0);
		ireq->ireq_state = TCP_NEW_SYN_RECV;
		write_pnet(&ireq->ireq_net, sock_net(sk_listener));
		ireq->ireq_family = sk_listener->sk_family;
	}

	return req;
}
EXPORT_SYMBOL(inet_reqsk_alloc);

 

 reqsk_alloc함수를 통해 request_sock 자료구조를 할당합니다. 그리고 몇 가지 초기화를 진행합니다.

 

5-3-1. reqsk_alloc

// include/net/request_sock.h/reqsk_alloc() :85

static inline struct request_sock *
reqsk_alloc(const struct request_sock_ops *ops, struct sock *sk_listener,
	    bool attach_listener)
{
	struct request_sock *req;

	req = kmem_cache_alloc(ops->slab, GFP_ATOMIC | __GFP_NOWARN);
	if (!req)
		return NULL;
	req->rsk_listener = NULL;
	if (attach_listener) {
		if (unlikely(!refcount_inc_not_zero(&sk_listener->sk_refcnt))) {
			kmem_cache_free(ops->slab, req);
			return NULL;
		}
		req->rsk_listener = sk_listener;
	}
	req->rsk_ops = ops;
	req_to_sk(req)->sk_prot = sk_listener->sk_prot;
	sk_node_init(&req_to_sk(req)->sk_node);
	sk_tx_queue_clear(req_to_sk(req));
	req->saved_syn = NULL;
	req->num_timeout = 0;
	req->num_retrans = 0;
	req->sk = NULL;
	refcount_set(&req->rsk_refcnt, 0);

	return req;
}

 

이 함수는 실제로 kernel 내 메모리 공간을 할당합니다. 

 

  • attach_listener는 true이므로 req->rsk_listener에 listen socket sk가 붙습니다.
  • rsk_ops는 tcp_conn_request에서 넘어온 &tcp_request_sock_ops가 붙습니다.
  • sk_prot는 listen socket의 sk_prot인 &tcp_prot를 가져옵니다.
  • 그 외 몇몇 변수들을 초기화 합니다.

 

5-3-2. request_sock 구조체

request sock 구조체 자료구조는 다음과 같이 생겼습니다.

// include/net/request_sock.h :51

/* struct request_sock - mini sock to represent a connection request
 */
struct request_sock {
	struct sock_common		__req_common;
#define rsk_refcnt			__req_common.skc_refcnt
#define rsk_hash			__req_common.skc_hash
#define rsk_listener			__req_common.skc_listener
#define rsk_window_clamp		__req_common.skc_window_clamp
#define rsk_rcv_wnd			__req_common.skc_rcv_wnd

	struct request_sock		*dl_next;
	u16				mss;
	u8				num_retrans; /* number of retransmits */
	u8				syncookie:1; /* syncookie: encode tcpopts in timestamp */
	u8				num_timeout:7; /* number of timeouts */
	u32				ts_recent;
	struct timer_list		rsk_timer;
	const struct request_sock_ops	*rsk_ops;
	struct sock			*sk;
	struct saved_syn		*saved_syn;
	u32				secid;
	u32				peer_secid;
};

 

 

5-4. inet_reqsk_alloc에서 초기화 더 수행

// net/ipv4/tcp_input.c/inet_reqsk_alloc() :6691

	if (req) {
		struct inet_request_sock *ireq = inet_rsk(req);

		ireq->ireq_opt = NULL;
#if IS_ENABLED(CONFIG_IPV6)
		ireq->pktopts = NULL;
#endif
		atomic64_set(&ireq->ir_cookie, 0);
		ireq->ireq_state = TCP_NEW_SYN_RECV;
		write_pnet(&ireq->ireq_net, sock_net(sk_listener));
		ireq->ireq_family = sk_listener->sk_family;
	}

 

이제 reqsk_alloc을 통해 공간을 할당 받았고 몇몇개는 초기화 했습니다. 남은 요소들을 추가로 초기화합니다.

 

대부분 listener sock에 있던 데이터들을 가져옵니다. sk_family와 같은 경우는 socket() system call에서 AF_INET으로 초기화가 됐었습니다.

 

inet_rsk는 request_sock을 inet_request_sock으로 타입을 확장합니다. inet_request_sock 구조체는 다음과 같이 생겼습니다.

 

5-4-1. inet_request_sock 구조체

// include/net/inet_sock.h :68

struct inet_request_sock {
	struct request_sock	req;
#define ir_loc_addr		req.__req_common.skc_rcv_saddr
#define ir_rmt_addr		req.__req_common.skc_daddr
#define ir_num			req.__req_common.skc_num
#define ir_rmt_port		req.__req_common.skc_dport
#define ir_v6_rmt_addr		req.__req_common.skc_v6_daddr
#define ir_v6_loc_addr		req.__req_common.skc_v6_rcv_saddr
#define ir_iif			req.__req_common.skc_bound_dev_if
#define ir_cookie		req.__req_common.skc_cookie
#define ireq_net		req.__req_common.skc_net
#define ireq_state		req.__req_common.skc_state
#define ireq_family		req.__req_common.skc_family

	u16			snd_wscale : 4,
				rcv_wscale : 4,
				tstamp_ok  : 1,
				sack_ok	   : 1,
				wscale_ok  : 1,
				ecn_ok	   : 1,
				acked	   : 1,
				no_srccheck: 1,
				smc_ok	   : 1;
	u32                     ir_mark;
	union {
		struct ip_options_rcu __rcu	*ireq_opt;
#if IS_ENABLED(CONFIG_IPV6)
		struct {
			struct ipv6_txoptions	*ipv6_opt;
			struct sk_buff		*pktopts;
		};
#endif
	};
};

 

 

5-5. tcp_conn_request로 돌아와서

// net/ipv4/tcp_input.c/tcp_conn_request() :6835

	req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);
	if (!req)
		goto drop;

 

5-3~5-4를 통해서 inet_reqsk_alloc과정을 살펴봤습니다. 이제 남은 내용을 확인해봅니다.

 

// net/ipv4/tcp_input.c/tcp_conn_reqest() :6839

	req->syncookie = want_cookie;
	tcp_rsk(req)->af_specific = af_ops;
	tcp_rsk(req)->ts_off = 0;
#if IS_ENABLED(CONFIG_MPTCP)
	tcp_rsk(req)->is_mptcp = 0;
#endif

	tcp_clear_options(&tmp_opt);
	tmp_opt.mss_clamp = af_ops->mss_clamp;
	tmp_opt.user_mss  = tp->rx_opt.user_mss;
	tcp_parse_options(sock_net(sk), skb, &tmp_opt, 0,
			  want_cookie ? NULL : &foc);

	if (want_cookie && !tmp_opt.saw_tstamp)
		tcp_clear_options(&tmp_opt);

	if (IS_ENABLED(CONFIG_SMC) && want_cookie)
		tmp_opt.smc_ok = 0;

	tmp_opt.tstamp_ok = tmp_opt.saw_tstamp;
	tcp_openreq_init(req, &tmp_opt, skb, sk);
	inet_rsk(req)->no_srccheck = inet_sk(sk)->transparent;

 

syncookie에 want_cookie를 저장, tcp_rsk(req)에 몇몇 값들을 초기화합니다. tcp_rsk(req)는 마찬가지로 request sock을 tcp_request_sock구조체로 형변환을 한 것입니다.

 

tcp_request_sock 구조체는 다음과 같이 생겼습니다.

// include/linux/tcp.h :119

struct tcp_request_sock {
	struct inet_request_sock 	req;
	const struct tcp_request_sock_ops *af_specific;
	u64				snt_synack; /* first SYNACK sent time */
	bool				tfo_listener;
	bool				is_mptcp;
#if IS_ENABLED(CONFIG_MPTCP)
	bool				drop_req;
#endif
	u32				txhash;
	u32				rcv_isn;
	u32				snt_isn;
	u32				ts_off;
	u32				last_oow_ack_time; /* last SYNACK */
	u32				rcv_nxt; /* the ack # by SYNACK. For
						  * FastOpen it's the seq#
						  * after data-in-SYN.
						  */
	u8				syn_tos;
};

 

mptcp는 multi-path tcp의 약자인데, 이 내용은 스킵하겠습니다.

 

tcpreqsk의 af_specific에는 tcp_conn_request에서 넘어온 &tcp_request_sock_ipv4가 붙습니다.

 

5-5-1. tcp options

// linux/tcp.h/tcp_clear_options() :102

static inline void tcp_clear_options(struct tcp_options_received *rx_opt)
{
	rx_opt->tstamp_ok = rx_opt->sack_ok = 0;
	rx_opt->wscale_ok = rx_opt->snd_wscale = 0;
#if IS_ENABLED(CONFIG_SMC)
	rx_opt->smc_ok = 0;
#endif
}

 

tmp_opt 구조체의 값들을 초기화합니다.

 

그 아래 나오는 tcp_parse_options는 수신한 sk_buff에서 tcp_hdr에 붙은 option들을 parsing하여 분류해줍니다. 그리고 이를 tmp_opt 구조체에 값들을 넣어서 돌려줍니다.

	tcp_parse_options(sock_net(sk), skb, &tmp_opt, 0,
			  want_cookie ? NULL : &foc);

 

그 다음은 want_cookie가 true이면서 saw_tstamp가 0인경우는 tmp_opt를 다시 초기화합니다. 현재 우리는 want_cookie를 false라고 가정하고 있으므로 넘어갑니다. (코드의 의미는 추후 확인 필요)

	if (want_cookie && !tmp_opt.saw_tstamp)
		tcp_clear_options(&tmp_opt);
        
	if (IS_ENABLED(CONFIG_SMC) && want_cookie)
		tmp_opt.smc_ok = 0;

 

 

5-5-2. tcp_openreq_init

수신된 packet의 tcp option을 가지고 몇몇 변수를 추가로 초기화합니다.

// net/ipv4/tcp_input.c/tcp_openreq_init() :6664

static void tcp_openreq_init(struct request_sock *req,
			     const struct tcp_options_received *rx_opt,
			     struct sk_buff *skb, const struct sock *sk)
{
	struct inet_request_sock *ireq = inet_rsk(req);

	req->rsk_rcv_wnd = 0;		/* So that tcp_send_synack() knows! */
	tcp_rsk(req)->rcv_isn = TCP_SKB_CB(skb)->seq;
	tcp_rsk(req)->rcv_nxt = TCP_SKB_CB(skb)->seq + 1;
	tcp_rsk(req)->snt_synack = 0;
	tcp_rsk(req)->last_oow_ack_time = 0;
	req->mss = rx_opt->mss_clamp;
	req->ts_recent = rx_opt->saw_tstamp ? rx_opt->rcv_tsval : 0;
	ireq->tstamp_ok = rx_opt->tstamp_ok;
	ireq->sack_ok = rx_opt->sack_ok;
	ireq->snd_wscale = rx_opt->snd_wscale;
	ireq->wscale_ok = rx_opt->wscale_ok;
	ireq->acked = 0;
	ireq->ecn_ok = 0;
	ireq->ir_rmt_port = tcp_hdr(skb)->source;
	ireq->ir_num = ntohs(tcp_hdr(skb)->dest);
	ireq->ir_mark = inet_request_mark(sk, skb);
#if IS_ENABLED(CONFIG_SMC)
	ireq->smc_ok = rx_opt->smc_ok;
#endif
}

 

 

5-5-3. route_req

// net/ipv4/tcp_input.c/tcp_conn_request() :6862

	/* Note: tcp_v6_init_req() might override ir_iif for link locals */
	inet_rsk(req)->ir_iif = inet_request_bound_dev_if(sk, skb);

	dst = af_ops->route_req(sk, skb, &fl, req);
	if (!dst)
		goto drop_and_free;

	if (tmp_opt.tstamp_ok)
		tcp_rsk(req)->ts_off = af_ops->init_ts_off(net, skb);

 

inet_request_bound_dev_if는 넘기도록 하겠습니다.

 

이제 SYN|ACK을 보내기 위해서 dst_entry를 알아내야 합니다. af_ops는 tcp_request_sock_ipv4_ops였고, 이 함수에서 route_req는 tcp_v4_route_req가 연결되어 있습니다.

 

5-6. tcp_v4_route_req

// net/ipv4/tcp_ipv4.c/tcp_v4_route_req() :1481

static struct dst_entry *tcp_v4_route_req(const struct sock *sk,
					  struct sk_buff *skb,
					  struct flowi *fl,
					  struct request_sock *req)
{
	tcp_v4_init_req(req, sk, skb);

	if (security_inet_conn_request(sk, skb, req))
		return NULL;

	return inet_csk_route_req(sk, &fl->u.ip4, req);
}

 

5-6-1. tcp_v4_init_req

// net/ipv4/tcp_ipv4.c/tcp_vr_init_req() :1469

static void tcp_v4_init_req(struct request_sock *req,
			    const struct sock *sk_listener,
			    struct sk_buff *skb)
{
	struct inet_request_sock *ireq = inet_rsk(req);
	struct net *net = sock_net(sk_listener);

	sk_rcv_saddr_set(req_to_sk(req), ip_hdr(skb)->daddr);
	sk_daddr_set(req_to_sk(req), ip_hdr(skb)->saddr);
	RCU_INIT_POINTER(ireq->ireq_opt, tcp_v4_save_options(net, skb));
}

 

해당 함수는 request sock의 source address를 설정합니다. 그 주소는 skb의 목적지 주소에서 가져옵니다.

 

sk_rcv_saddr_set 함수와 sk_daddr_set 함수는 다음과 같이 생겼습니다.

 

// include/net/inet_hashtables.h/sk_rcv_saddr_set() :419

static inline void sk_rcv_saddr_set(struct sock *sk, __be32 addr)
{
	sk->sk_rcv_saddr = addr; /* alias of inet_rcv_saddr */
#if IS_ENABLED(CONFIG_IPV6)
	ipv6_addr_set_v4mapped(addr, &sk->sk_v6_rcv_saddr);
#endif
}

 

// include/net/inet_hashtables.h/sk_daddr_set() :411

static inline void sk_daddr_set(struct sock *sk, __be32 addr)
{
	sk->sk_daddr = addr; /* alias of inet_daddr */
#if IS_ENABLED(CONFIG_IPV6)
	ipv6_addr_set_v4mapped(addr, &sk->sk_v6_daddr);
#endif
}

 

tcp_v4_save_options는 ip layer option을 처리합니다. 따라서 현재의 관심사가 아니므로 넘어가겠습니다.

 

5-6-2. inet_csk_route_req

inet_csk_route_req는 라우팅 테이블을 확인해서 목적지 대상을 가져옵니다. dst_entry는 다음과 같이 생겼습니다.

// include/net/dst.h :25

struct dst_entry {
	struct net_device       *dev;
	struct  dst_ops	        *ops;
	unsigned long		_metrics;
	unsigned long           expires;
#ifdef CONFIG_XFRM
	struct xfrm_state	*xfrm;
#else
	void			*__pad1;
#endif
	int			(*input)(struct sk_buff *);
	int			(*output)(struct net *net, struct sock *sk, struct sk_buff *skb);

	unsigned short		flags;
#define DST_NOXFRM		0x0002
#define DST_NOPOLICY		0x0004
#define DST_NOCOUNT		0x0008
#define DST_FAKE_RTABLE		0x0010
#define DST_XFRM_TUNNEL		0x0020
#define DST_XFRM_QUEUE		0x0040
#define DST_METADATA		0x0080

	/* A non-zero value of dst->obsolete forces by-hand validation
	 * of the route entry.  Positive values are set by the generic
	 * dst layer to indicate that the entry has been forcefully
	 * destroyed.
	 *
	 * Negative values are used by the implementation layer code to
	 * force invocation of the dst_ops->check() method.
	 */
	short			obsolete;
#define DST_OBSOLETE_NONE	0
#define DST_OBSOLETE_DEAD	2
#define DST_OBSOLETE_FORCE_CHK	-1
#define DST_OBSOLETE_KILL	-2
	unsigned short		header_len;	/* more space at head required */
	unsigned short		trailer_len;	/* space to reserve at tail */

	/*
	 * __refcnt wants to be on a different cache line from
	 * input/output/ops or performance tanks badly
	 */
#ifdef CONFIG_64BIT
	atomic_t		__refcnt;	/* 64-bit offset 64 */
#endif
	int			__use;
	unsigned long		lastuse;
	struct lwtunnel_state   *lwtstate;
	struct rcu_head		rcu_head;
	short			error;
	short			__pad;
	__u32			tclassid;
#ifndef CONFIG_64BIT
	atomic_t		__refcnt;	/* 32-bit offset 64 */
#endif
};

 

L3와 라우팅 테이블은 다음에 살펴보도록 하겠습니다.

 

어쨌든 af_ops->route_req를 통해서 SYN | ACK을 보낼 경로를 설정합니다.

 

중간점검

현재까지의 request sock 값들이 우리가 예상한 값들과 같은지 한 번 출력해봅시다. 다음처럼 6865줄 옆에 우리가 만들 함수를 호출해줍시다.

dst = af_ops->route_req(sk, skb, &fl, req); byeo_print_reqsk_data(req);

 

void byeo_print_reqsk_data(struct request_sock *req)
{
	struct sock_common* rsk_common = &req_to_sk(req)->__sk_common;
	struct inet_request_sock *ireq = inet_rsk(req);
	struct tcp_request_sock *tcpreq = tcp_rsk(req);

	printk("[byeo:req:sk_common] tx_queue_mapping %d, refcnt %p, cookie %ld, rcvwnd %d, state %d, net %p, family %d\n", rsk_common->skc_tx_queue_mapping, rsk_common->skc_refcnt.refs, rsk_common->skc_cookie, rsk_common->skc_rcv_wnd, rsk_common->skc_state, rsk_common->skc_net, rsk_common->skc_family);
	printk("[byeo:req:sk_common] dport %d, num %d, rcv_saddr %d, daddr %d\n", rsk_common->skc_dport, rsk_common->skc_num, rsk_common->skc_rcv_saddr, rsk_common->skc_daddr);
	printk("[byeo:req:req] mss %d, num_retrans %d, syncookie %d, num_timeout %d, ts_recent %d, rsk_timer %p, saved_syn %p\n", req->mss, req->num_retrans, req->syncookie, req->num_timeout, req->ts_recent, req->rsk_timer, req->saved_syn);
	printk("[byeo:req:ireq] ireq_opt %p, sack_ok %d, wscale_ok %d, acked %d, ecn_ok %d, ir_mark %d\n", ireq->ireq_opt, ireq->sack_ok, ireq->wscale_ok, ireq->acked, ireq->ecn_ok, ireq->ir_mark);
	printk("[byeo:req:tcpreq] ts_off %d, rcv_isn %d, rcv_nxt %d, snt_synack %lld, last_oow_ack_time %d\n", tcpreq->ts_off, tcpreq->rcv_isn, tcpreq->rcv_nxt, tcpreq->snt_synack, tcpreq->last_oow_ack_time);
}

 

 

rcv_saddr: 335544330 = 0x14 00 00 0A = 20.0.0.10 -> ntoh -> 10.0.0.20

daddr: 16777226 = 0x01 00 00 0A = 1.0.0.10 -> ntoh -> 10.0.0.1 

 

반응형

'프로그래밍 (Programming) > 네트워크 스택' 카테고리의 다른 글

TCP SYN_RECV state의 ACK 처리 1  (0) 2024.06.02
Listen socket의 TCP SYN 처리 2  (0) 2024.05.31
ftrace 사용법  (0) 2024.05.23
accept system call 3 (inet_accept)  (0) 2024.05.19
[Linux Kernel] Blocking I/O  (0) 2024.05.17
Comments