套接字分析

每个操作系统都必须提供网络系统入口和 API,Linux 内核网络子系统提供的标准 POSIX 套接字向用户提供接口。在 Linux 传输层之上的一切都属于用户空间。Linux 也遵循 Unix 范式(一切皆文件),因此套接字也与文件相关联,使用统一套接字 API 会让应用程序移植更容易,如下为套接字类型:

  • SOCK_STREAM:流套接字,提供可靠的字节流通信信道。TCP 套接字就属于流套接字;
  • SOCK_DGRAM:数据报套接字,支持消息交换。信道不可靠,因为数据包可能被丢弃、不按顺序或者重复;
  • SOCK_RAW:原始套接字,直接访问 IP 层,支持使用协议无关的传输层格式收发数据流;
  • SOCK_RDM:可靠传输消息,用于透明进程间通信;
  • SOCK_SEQPACKET:顺序数据包流,这种套接字类似流套接字,也是面向连接的;
  • SOCK_DCCP:数据报拥塞控制协议是一种传输层协议,提供不可靠数据包拥塞控制流。

套接字

在内核中,有两个表示套接字结构,一个是 socket,另一个 socksocket 具体内核源码主要成员如下:

image.png

image.png

sock 具体内核源码主要成员如下:

image.png

image.png

从用户空间套接字发送数据或在用户空间套接字中接收来自传输层的数据,这些工作分配时通信内核中调用方法 sendmsg()/recvmsg() 来处理。它们会将一个 msghdr 对象作为参数,这个 msghdr 对象包含要发送或填充的数据块及其他参数。

image.png

用户数据包协议 UDP

UDP 提供面向消息的不可靠传输,但没有拥塞控制功能。很多协议都使用 UDP,如用于 IP 网络传输音频和视频的实时传输协议(Real-time Transport Protocol,RTP),此类型容许一定的数据包丢弃。UDP 报头长 8 字节,内核源码如下:

image.png

UDP 初始化操作

定义对象 udp_protocol 并使用方法 inet_add_protocol() 来添加它:

image.png

image.png

image.png

发送 UDP 数据包

从 UDP 用户空间套接字中发送数据,可使用系统调用:send(),sendto(),sendmsg()和 write()。这些系统调用最终都会由内核中方法 udp_sendmsg() 来处理。具体内核源码如下:

image.png

接收 L3 的 UDP 数据包

方法 udp_rcv() 是负责接收来自 L3 的 UDP 数据包主处理程序,调用方法如下:

image.png

int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable,
           int proto)
{
    struct sock *sk;
    struct udphdr *uh;
    unsigned short ulen;
    struct rtable *rt = skb_rtable(skb);
    __be32 saddr, daddr;
    struct net *net = dev_net(skb->dev);

    /*
     *  Validate the packet.
     */
    if (!pskb_may_pull(skb, sizeof(struct udphdr)))
        goto drop;      /* No space for header. */

    // 从SKB中取UDP头、报头长度和源地址、目的地址
    uh   = udp_hdr(skb);
    ulen = ntohs(uh->len);
    saddr = ip_hdr(skb)->saddr;
    daddr = ip_hdr(skb)->daddr;

    if (ulen > skb->len)
        goto short_packet;

    if (proto == IPPROTO_UDP) {
        /* UDP validates ulen. */
        if (ulen < sizeof(*uh) || pskb_trim_rcsum(skb, ulen))
            goto short_packet;
        uh = udp_hdr(skb);
    }

    if (udp4_csum_init(skb, uh, proto))
        goto csum_error;

    sk = skb_steal_sock(skb);
    if (sk) {
        struct dst_entry *dst = skb_dst(skb);
        int ret;

        if (unlikely(sk->sk_rx_dst != dst))
            udp_sk_rx_dst_set(sk, dst);

        ret = udp_unicast_rcv_skb(sk, skb, uh);
        sock_put(sk);
        return ret;
    }
    // 完整性检查,如果确保UDP头不超过数据包疮毒且核实指定proto为UDP协议标识IPPROTO_UDP
    // 如果数据包为广播或组播,调用方法__udp4_lin_mcast_deliver
    if (rt->rt_flags & (RTCF_BROADCAST|RTCF_MULTICAST))
        return __udp4_lib_mcast_deliver(net, skb, uh,
                        saddr, daddr, udptable, proto);

    // 接下来,在UDP套接字散列表中查找
    sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);
    if (sk)
        // 找到匹配,对SKB做进一步处理
        return udp_unicast_rcv_skb(sk, skb, uh);

    if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
        goto drop;
    nf_reset_ct(skb);

    /* No socket. Drop packet silently, if checksum is wrong */
    // 没有匹配的套接字,如果校验不对,丢弃数据包
    if (udp_lib_checksum_complete(skb))
        goto csum_error;

    __UDP_INC_STATS(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
    icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);

    /*
     * Hmm.  We got an UDP packet to a port to which we
     * don't wanna listen.  Ignore it.
     */
    kfree_skb(skb);
    return 0;

short_packet:
    net_dbg_ratelimited("UDP%s: short packet: From %pI4:%u %d/%d to %pI4:%u\n",
                proto == IPPROTO_UDPLITE ? "Lite" : "",
                &saddr, ntohs(uh->source),
                ulen, skb->len,
                &daddr, ntohs(uh->dest));
    goto drop;

csum_error:
    /*
     * RFC1122: OK.  Discards the bad packet silently (as far as
     * the network is concerned, anyway) as per 4.1.3.4 (MUST).
     */
    net_dbg_ratelimited("UDP%s: bad checksum. From %pI4:%u to %pI4:%u ulen %d\n",
                proto == IPPROTO_UDPLITE ? "Lite" : "",
                &saddr, ntohs(uh->source), &daddr, ntohs(uh->dest),
                ulen);
    __UDP_INC_STATS(net, UDP_MIB_CSUMERRORS, proto == IPPROTO_UDPLITE);
drop:
    __UDP_INC_STATS(net, UDP_MIB_INERRORS, proto == IPPROTO_UDPLITE);
    kfree_skb(skb);
    return 0;
}

UDP 数据包接收流程

image.png

传输控制协议 TCP

TCP 是 Internet 中最常用的传输协议,很多著名的协议都基于 TCP。其中最著名的协议都基于 TCP。其中最著名的可能就是 HTTP,但是这里有必要提及一些著名协议,如 SSH,SMTP,SSL 等。不同于 UDP 的是 TCP 提供面向连接的可靠传输,是通过使用序列号和确认来实现。

TCP 头是 20 个字节,不过在使用 TCP 选项时他的最长字节可以达到 60 个字节,具体如下:

image.png

image.png

TCP 初始化操作

image.png

image.png

TCP 定时器和 TCP 套接字初始化操作

  • TCP 使用定时器有 4 个:重传定时器、厑确认定时器、存活定时器、持续定时器。
  • 使用 TCP 套接字,用户空间应用程序必须创建一个 SOCK_STREAM 套接字,且调用系统调用 socket(),内核中由回调函数 tcp_v4_init_sock() 来处理,实际完成工作由 tcp_init_sock()
  • TCP 连接的建立和拆除及 TCP 连接的属性都被描述为状态机的状态,在给定时间点,TCP 套接字将处于指定的任何一种状态。在 TCP 客户端和 TCP 服务器之间,使用三次握手 TCP 连接。

TCP 定时器(net/ipv4/tcp_timer.c)

  • 重传定时器:负责重传在指定时间内没有得到确认的数据包;
  • 延迟确认定时器:推迟发送确认数据包;
  • 存活定时器:检查连接是否断开;
  • 零窗口探测定时器(持续定时器):缓冲区满后,接收方会通告零窗口,发送方将停止发送数据。

要使用 TCP 套接字,用户空间应用必须创建一个 SOCK_STREAM 套接字,并调用系统调用 socket(),在内核里由回调函数 tcp_v4_init_socket() 来处理。实际工作由 tcp_init_sock() 来完成处理所有操作,主要任务如下:

  1. 将套接字的状态设置为 TCO_CLOSE
  2. 调用方法 tcp_init_xmit_timers() 来初始化 TCP 定时器;
  3. 初始化套接字的发送缓冲区(sk_sndbuf)和接收缓冲区(sk_rcvbuf);
  4. 初始化无序队列和预备队列;
  5. 初始化各种参数。

image.png

在 TCP 客户端和 TCP 服务器之间,使用三次握手来建立 TCP 连接顺序如下:

  1. 首先,客户端向服务器发送 SYN 请求,其状态变为 TCP_SYN_SEND
  2. 帧听的服务器套接字(其状态为 TCP_LISTEN)创建一个处于 TCP_SYN_RECV 状态的请求套接字,来表示新的连接,并发回一个 SYN_ACK
  3. 客户端收到 SYN_ACK 后,将其状态变为 TCP_ESTABLISHED,并向服务器发送一个 ACK;
  4. 服务器收到 ACK 后,将请求套接字修改为处于 TCP_ESTABLISHED 状态的子套接字,因为此时连接已经建立,可以发送数据。

接收 L3 的 TCP 数据包

方法 tcp_v4_rcv 是负责接收来自 L3 的 TCP 数据包的主处理程序,内核源码具体如下:

image.png

image.png

发送 TCP 数据包

从用户空间中创建的 TCP 套接字发送数据包,可使用多个系统调用,包括 send(),sendto(),sendmsg()和 write(),系统调用最终由方法 tcp_sendmsg() 处理,它将来自用户空间的有效负载复制到内核,将其作为 TCP 数据段发送。

image.png