网络协议栈(2)connect超时

一、网络问题

如果世界都是像童话中描述的那样,那我们就真的和谐了,但是事实上往往是残缺不全。当我们在分析网络协议的时候,如果网络都是想美帝那样流畅,那报文的发送就没有问题了。但是现在如果不幸的是如果网络质量很差,那么此时就会出现TCP的丢包问题,此时大家都觉得网络很卡,比方说,WAR3就没法玩了。

当然网络中还潜伏着各种的好事之徒,随时等待网络配置的漏洞来控制主机。还有专门的工具例如nmap来扫描系统中电脑所有已经打开的端口,从而尝试远程控制主机,此时都给网络添加了很多的不确定性。

二、TCP网络重传

1、安全问题

现在假设有人要尝试攻击一个主机,那么最基本的功课就是开始扫描这个主机所有的打开端口。如何扫描呢?最黄最暴力的(有时候也是最有效)方法就是在本机创建一个socket,然后以目标机为服务器,通过connect系统调用来尝试练级服务器。如果收到服务器的回应,那么就算这个服务器端口是打开的。或者即使只扫描目标机的一些常用(Well-known)端口,也够主机喝一壶了。

但是这么我都能想到的方法,肯定是可以被轻松预防的。例如,今天我就写了个小程序试了一下,结果比较悲剧,我这个connect大概等了几分钟,然后返回连接超时。这也就是说,服务器根本没有给我回应,进一步说,这个服务器可能开了防火墙。这样一来,扫描一个端口就需要几分钟,那么要扫到1024,那就是一天多的时间,这个还没有考虑到更为警惕的目标机,例如主动发现这种暴力扫描的方法。

2、正常的无侦听连接

假设说一个目标机很傻很天真,对于任何发来的连接请求都进行一个处理,那么客户端的用户代码会从connect系统调用返回一个ECONNREFUSED错误码,也就是连接被拒绝。现在我们就看一下网络中客户端和客户端这样的一个比较友好的交互过程。

tcp_v4_rcv

 sk = __inet_lookup(&tcp_hashinfo, skb->nh.iph->saddr, th->source,
      skb->nh.iph->daddr, th->dest,
      inet_iif(skb));

 if (!sk)
  goto no_tcp_socket;

……

no_tcp_socket:
 if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
  goto discard_it;

 if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
bad_packet:
  TCP_INC_STATS_BH(TCP_MIB_INERRS);
 } else {
  tcp_v4_send_reset(NULL, skb);
 }
如果说没有套接口在这个地方进行侦听,那么__inet_lookup函数是无法找到一个对应的sock,所以也就是满足了接下来的一个简单判断,从而直接跳转到no_tcp_socket跳转点。由于这是一个合法的报文,所以套接口就会执行到发送reset的接口。

tcp_v4_send_reset

……

 /* Swap the send and the receive. */
 memset(&rep, 0, sizeof(rep));
 rep.th.dest   = th->source;
 rep.th.source = th->dest;
 rep.th.doff   = sizeof(struct tcphdr) / 4;
 rep.th.rst    = 1;这里做的非常简单,就是交换了发送方和接收方,然后让这个报文掉头回去,最为精髓的就是设置了报文的rst标志位(一个bit)。

现在看看那个已经发送了SYN的客户端收到这个报文会不会内牛满面。

tcp_v4_rcv--->>>tcp_v4_do_rcv---->>>tcp_rcv_state_process--->>tcp_rcv_synsent_state_process


 if (th->ack) {
  /* rfc793:
   * "If the state is SYN-SENT then
   *    first check the ACK bit
   *      If the ACK bit is set
   *   If SEG.ACK =< ISS, or SEG.ACK > SND.NXT, send
   *        a reset (unless the RST bit is set, if so drop
   *        the segment and return)"
   *
   *  We do not send data with SYN, so that RFC-correct
   *  test reduces to:
   */

……

if (th->rst) {
   tcp_reset(sk);也就是在发送端收到这个rst标志之后,将会执行tcp_reset接口,从而完成自我救赎。
   goto discard;
  }

static void tcp_reset(struct sock *sk)
{
 /* We want the right error as BSD sees it (and indeed as we do). */
 switch (sk->sk_state) {
  case TCP_SYN_SENT:
   sk->sk_err = ECONNREFUSED这里就返回了用户态看到的一个连接被拒绝的错误提示

……

tcp_done(sk);

而在tcp_done函数中,其中将会执行sk->sk_state_change(sk);,这个也就是sock_init_data中注册的sk->sk_state_change = sock_def_wakeup;函数,这个函数就会唤醒在inet_csk_wait_for_connect中等待的connect函数,这个系统调用就悻悻而回了。

3、如果服务器直接无视SYNC

现在假设服务器比较清高,叼都不叼你,连个reset消息都懒得回,那客户端就只能在这里苦苦等待了。这个场景比较直观,不直观的是客户端大概会等多久。

从inet_csk_wait_for_connect函数中看,其中的 schedule_timeout(timeo)中的timeo变量时无限等待(0xFFFFFFFF),也就是乍一看是海枯石烂,地老天荒。但是好事的我试了一下,发现没有那么长时间(至少在我有生之年是看到这个connect函数返回了),大概是几分钟的时间,那我们就再无聊一些,看看这个等待时间是怎么算的。

我们知道,TCP是一个有连接的网络传送协议,所以其中添加了很多的定时器,每当一个报文发送之后,TCP都会多一个心眼,就是创建一个重传定时器,这样如果在定时器超期之后,还没有发送完成,那就需要重传了。在tcp_v4_init_sock函数中,其中添加了

 tcp_init_xmit_timers(sk);
 tcp_prequeue_init(tp);

 icsk->icsk_rto = TCP_TIMEOUT_INIT;

#define TCP_TIMEOUT_INIT ((unsigned)(3*HZ)) /* RFC 1122 initial RTO value */
也就是最开始的时候,超时时间设置为3s,如果这个事件之内没有收到回应,那么定时器就开始发作了,执行tcp_init_xmit_timers函数中注册的重传定时器。这里我们得到的就是tcp_retransmit_timer函数,这个函数从名字上看就是一个重传定时器,事实上它的确是。

 if (tcp_write_timeout(sk))
  goto out;
……

icsk->icsk_backoff++;
 icsk->icsk_retransmits++;

out_reset_timer:
 icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX);这里比较精髓,就是下次重传的时间是按倍数递增的,所以也就是第一次发送的时候如果失败,下次就隔6,下下次12、下下下次24……,但是有个上限,就是TCP_RTO_MAX,这个值为120秒,大概2分钟。这段代码之前有一个注释,由于比较长,我就没有摘,但是我很欣赏这个注释风格。作者说,如果以后要实现一个到火星的ftp连接,这个120s是还可以再增大一点的。
 inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, icsk->icsk_rto, TCP_RTO_MAX);

最后是看看这个超时的操作


/* A write timeout has occurred. Process the after effects. */
static int tcp_write_timeout(struct sock *sk)
{
 struct inet_connection_sock *icsk = inet_csk(sk);
 struct tcp_sock *tp = tcp_sk(sk);
 int retry_until;
 int mss;

 if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {此时的发送套接口状态就是满足这里的限制的,根准确的说就是这里的那个TCPF_SYN_SENT状态,所以这里的retry_until值就是初始化wiesysctl_tcp_syn_retries值,这个值在proc中可以看到,默认值为5.
  if (icsk->icsk_retransmits)
   dst_negative_advice(&sk->sk_dst_cache);
  retry_until = icsk->icsk_syn_retries ? : sysctl_tcp_syn_retries;
 } else {
  if (icsk->icsk_retransmits >= sysctl_tcp_retries1) {
   /* Black hole detection */
   if (sysctl_tcp_mtu_probing) {
    if (!icsk->icsk_mtup.enabled) {
     icsk->icsk_mtup.enabled = 1;
     tcp_sync_mss(sk, icsk->icsk_pmtu_cookie);
    } else {
     mss = min(sysctl_tcp_base_mss,
        tcp_mtu_to_mss(sk, icsk->icsk_mtup.search_low)/2);
     mss = max(mss, 68 - tp->tcp_header_len);
     icsk->icsk_mtup.search_low = tcp_mss_to_mtu(sk, mss);
     tcp_sync_mss(sk, icsk->icsk_pmtu_cookie);
    }
   }

   dst_negative_advice(&sk->sk_dst_cache);
  }

  retry_until = sysctl_tcp_retries2;
  if (sock_flag(sk, SOCK_DEAD)) {
   const int alive = (icsk->icsk_rto < TCP_RTO_MAX);

   retry_until = tcp_orphan_retries(sk, alive);

   if (tcp_out_of_resources(sk, alive || icsk->icsk_retransmits < retry_until))
    return 1;
  }
 }

 if (icsk->icsk_retransmits >= retry_until) {
  /* Has it gone just too far? */
  tcp_write_err(sk);这里将会唤醒在connect中等待的系统调用。
  return 1;
 }
 return 0;
}
大致看了一下,其中值定义wie5,也就是最多重传试了5次

#define TCP_SYN_RETRIES  5 /* number of times to retry active opening a
     * connection: ~180sec is RFC minimum */

为了证实我没乱盖,看一下proc文件系统

[tsecer@Harry sockport]$ cat /proc/sys/net/ipv4/tcp_syn_retries 
5
不多不少,就是5.

现在我们就计算一下这重传等待时间:

3+6+12+24+……3*2^5=3(2^6-1)=3*63=189s

也就是大概3分钟零9秒的时间之后,这个connect返回失败。

tcp_write_err---->>>>tcp_done

……

if (!sock_flag(sk, SOCK_DEAD))
  sk->sk_state_change(sk);这里又把connect唤醒了,并且错误码为ETIMEDOUT,这个值在tcp_write_err中设置。
 else
  inet_csk_destroy_sock(sk);

三、TODO

这个rto是不是会自动恢复为原始值,是慢慢恢复还是直接回到3秒?

写个connect程序简单测一下这个时间是不是189秒。

posted on 2019-03-06 20:51  tsecer  阅读(658)  评论(0编辑  收藏  举报

导航