结合源码学习 TCP/IP 协议栈(1)

  • 前言

    课本上计网基本上是纯理论的东西 + 一坨考研真题,学完只能理解一个大致架构和少许的数学应用题解题能力。我还是认为作为一个网络安全专业的学生,在学习这些底层知识的时候,最好还是能从代码出发,一方面不看代码永远不会理解漏洞的本质,另一方面不会写代码的安全人员,他的成长也只能是从 jb 小子 -> jb 老子。

    主要参考自 https://github.com/EmbedHacker/level-ip 这个项目,同类的还有很多,只是比较方便的找到了原作者的 blog 和国内的教程(参考文献第一个)。

  • 整体架构

    该项目模拟了一个 linux 内核的 tcp 协议栈

    image-20220321153745494

    其中, level-ip 是运行在 linux 用户空间的协议栈,也是我们主要实现的部分,虚拟网卡/tun 则是用了 Linux 自带的 TUN/TAP。

    tun是网络层的虚拟网络设备,可以收发第三层数据报文包,如IP封包,因此常用于一些点对点IP隧道,例如OpenVPN,IPSec等。
    
    tap是链路层的虚拟网络设备,等同于一个以太网设备,它可以收发第二层数据报文包,如以太网数据帧。Tap最常见的用途就是做为虚拟机的网卡,因为它和普通的物理网卡更加相近,也经常用作普通机器的虚拟网卡。
    
  • 环境搭建及测试

    本地 WSL2 的 Ubuntu20.04 编译报错,换成 18.04

    git clone https://github.com/EmbedHacker/level-ip
    sudo apt install libcab-dev
    make all
    
    • 以太网 -> 物理网卡

      ping 一下百度确保能连网就行

    • 物理网卡 -> 虚拟网卡

      利用 iptables 设置 netfilter 过滤规则实现,首先设置 linux 的路由器功能

      sudo sysctl -w net.ipv4.ip_forward=1
      

      设置防火墙,允许虚拟网卡的数据包通过

      sudo iptables -I INPUT --source 10.0.0.0/24 -j ACCEPT
      

      配置 NAT,修改发送到互联网的数据包的源地址为 ens33(物理网卡)

      sudo iptables -t nat -I POSTROUTING --out-interface enp0s3 -j MASQUERADE
      

      设置物理网卡和虚拟网卡的相互转发

      #物理网卡转发虚拟网卡
      sudo iptables -I FORWARD --in-interface enp0s3 --out-interface tap0 -j ACCEPT
      #虚拟网卡转发物理网卡
      sudo iptables -I FORWARD --in-interface tap0 --out-interface enp0s3 -j ACCEPT
      
    • 虚拟网卡 -> level-ip

      通过 tun/tap驱动实现,位于 /dev/net/tun,具体原理。。。和项目没什么关系,略了。

    • level-ip -> 应用程序

      用我们自己写的 .so 文件(动态库)来接管应用程序中调用的在 linux 内核中的 Socket API,让数据走 level-ip。

      运行协议栈:

      ./lvl-ip
      

      在另一个端口运行应用程序

      cd tools
      ./level-ip ../apps/curl/curl www.baidu.com 80
      

      成功返回抓取结果。

  • 向下封装虚拟网卡

    TCP / IP 协议栈的实现过程可以看作层层加壳和层层脱壳,每向下一层就要加上下层对应的头部,每向上一层就会把下层的头部脱去。在这个项目中,首先写要实现的 level-ip 和下面的虚拟网卡这一层的交互。

    首先看 src\tuntap_if.c 文件,找到初始化的 tun_init

    void tun_init()
    {
        dev = calloc(10, 1);
        tun_fd = tun_alloc(dev);
    
        if (set_ip_up(dev) != 0) {
            print_err("ERROR when setting up if\n");
        }
    
        if (set_if_route(dev, taproute) != 0) {
            print_err("ERROR when setting route for if\n");
        }
    
        if (set_if_address(dev, tapaddr) != 0) {
            print_err("ERROR when setting addr for if\n");
        }
    }
    

    给 dev 开辟了块内存,然后跟进一下 tun_alloc,首先要打开 tun 文件(linux 一切皆文件,拿到文件描述符才能操作),然后是设置了网卡的属性,其中:

    IFF_TUN: 创建一个点对点设备
    IFF_TAP: 创建一个以太网设备
    IFF_NO_PI: 不包含包信息,默认的每个数据包当传到用户空间时,都将包含一个附加的包头来保存包信息
    

    这里选择创建了一个 TAP 设备用于和上层的 TCP/IP 协议通信,而设置 IFF_NO_PI 属性就不需要将数据包的信息提前预置到以太网帧中,而是我们手动加一个头用于上下层的识别。

    static int tun_alloc(char *dev)
    {
        struct ipreq ifr;
        int fd, err;
    
        if( (fd = open("/dev/net/tun", O_RDWR)) < 0 ) {
            perror("Cannot open TUN/TAP dev\n"
                        "Make sure one exists with " 
                        "'$ mknod /dev/net/tap c 10 200'");
            exit(1);
        }
    
        CLEAR(ifr);
        /* Flags: IFF_TUN   - TUN device (no Ethernet headers)
         *        IFF_TAP   - TAP device
         *
         *        IFF_NO_PI - Do not provide packet information
         */
         ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
         if( *dev ) {
             strncpy(ifr.ifr_name, dev, IFNAMSIZ);
         }
    
         if ( (err = ioctl(fd, TUNSETIFF, (void *) &ifr)) < 0 ) {
            perror("ERR: Could not ioctl tun");
            close(fd);
            return err;
         }
        // ioctl 是系统库函数,用于配置一些虚拟网卡属性
    
         strcpy(dev, ifr.ifr_name);
         return fd;
    }
    

    除此意外这个文件中的 tun_read 和 tun_write 就是对 read 和 write 做了简单的封装,方便直接操作 tun。

    int tun_read(char *buf, int len)
    {
        return read(tun_fd, buf, len);
    }
    
    int tun_write(char *buf, int len)
    {
        return write(tun_fd, buf, len);
    }
    
  • 向下封装以太网

    在上面那张结构图中,虚拟网卡的已经封装好了,物理网卡没法操作不用管,接着就是封装下面的数据链路层,也就是以太网头部(mac头部),结构:

    image-20220321214754763

    看一下代码中的实现:

    struct eth_hdr
    {
        uint8_t dmac[6];	// 目标主机物理地址
        uint8_t smac[6];	// 源主机物理地址
        uint16_t ethertype;	// 判断标志,用于区分 ip 和 arp 包
        uint8_t payload[];	// 变长数组
    } __attribute__((packed));	// 设置结构体按照实际占用字节数进行对齐
    

    同时,一台主机可能有多块网卡,在收发数据流的过程中,还要根据包中的地址信息,确定当前的收到的数据流该由哪块网卡处理,网卡的具体信息在 level-ip 中的 include\netdev.h 中实现:

    struct netdev {
        uint32_t addr;		// 本地网卡的 ip 地址
        uint8_t addr_len;	// 地址长度
        uint8_t hwaddr[6];	// 本地网卡的物理地址
        uint32_t mtu;		// 网卡的最大传输单元
    };
    

    接着看一下这个网卡管理的初始化

    void netdev_init(char *addr, char *hwaddr)
    {
        loop = netdev_alloc("127.0.0.1", "00:00:00:00:00:00", 1500);
        netdev = netdev_alloc("10.0.0.4", "00:0c:29:6d:50:25", 1500);
    }
    

    自定义了 ip 地址和 mac 地址,接着跟进 netdev_alloc,至此网卡初始化完成。

    static struct netdev *netdev_alloc(char *addr, char *hwaddr, uint32_t mtu)
    {
        struct netdev *dev = malloc(sizeof(struct netdev));
    
        dev->addr = ip_parse(addr);	// ip 十进制->二进制格式
    
        sscanf(hwaddr, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", &dev->hwaddr[0],
                                                        &dev->hwaddr[1],
                                                        &dev->hwaddr[2],
                                                        &dev->hwaddr[3],
                                                        &dev->hwaddr[4],
                                                        &dev->hwaddr[5]);
        // 把 mac 地址填充到 netdev 中
        dev->addr_len = 6;
        dev->mtu = mtu;
    
        return dev;
    }
    

    接着就开始写收发数据的操作,首先是发送接口 netdev_transmit

    int netdev_transmit(struct sk_buff *skb, uint8_t *dst_hw, uint16_t ethertype)
    {
        struct netdev *dev;
        struct eth_hdr *hdr;	// 即将填充的头部
        int ret = 0;
    
        dev = skb->dev;	// sk_buff 结构体用于记录待发送的数据帧和网卡型号
    
        skb_push(skb, ETH_HDR_LEN);	// ETH_HDR_LEN是 eth_hdr 结构体大小,把 sk_buff 前移 ETH_HDR_LEN 个长度,空出位置添加 eth_hdr
    
        hdr = (struct eth_hdr *)skb->data;
    
        memcpy(hdr->dmac, dst_hw, dev->addr_len);
        memcpy(hdr->smac, dev->hwaddr, dev->dadr_len);
    
        hdr->ethertype = htons(ethertype);
        eth_dbg("out", hdr);
        // 将信息填充到数据帧头部
    
        ret = tun_write((char *)skb->data, skb->len);
    	// 调用 write 相当于发送数据了
        return ret;
    }
    

    然后是数据流的接收 netdev_receive,

    static int netdev_receive(struct sk_buff *skb)
    {
        struct eth_hdr *hdr = eth_hdr(skb);	// 获取以太网头部数据
    
        eth_dbg("in", hdr);
    
        switch (hdr->ethertype) {	// 根据不同的头部进行不同的处理
            case ETH_P_ARP:
                arp_rcv(skb);
                break;
            case ETH_P_IP:
                ip_rcv(skb);
                break;
            case ETH_P_IPV6:
            default:
                printf("Unsupported ethertype %x\n", hdr->ethertype);
                free_skb(skb);
                break;
        }
        return 0;
    }
    

    看一下整个接收数据的过程,其中在 netdev_rx_loop 中,首先实例化一个 skb 结构体,用 read 函数将接收的数据存到 skb 结构体中,然后调用 netdev_receive 解析。netdev_rx_loop 直接是在 main 函数中创建的线程。

    void *netdev_rx_loop()
    {
        while (running) {
            struct sk_buff *skb = alloc_skb(BUFLEN);
            
            if (tun_read((char *)skb->data, BUFLEN) < 0) { 
                perror("ERR: Read from tun_fd");
                free_skb(skb);
                return NULL;
            }
    
            netdev_receive(skb);
        }
    
        return NULL;
    }
    
  • ARP 相关实现

    ARP协议是用来将目标主机的IP地址转换为对应的以太网(MAC)地址的,要实现通过发起 ARP 查询帧,在本地建立 IP 地址和 MAC 的映射关系,同样也要实现应答其他的 ARP 查询。ARP 数据帧位于以太网数据帧上一次,报文结构如下:

    image-20220322094242803

    硬件协议:以太网 1
    协议类型:mac -> ip 0x0800
    硬件地址长度:mac 地址长度 6
    协议地址长度:ip 地址长度 6
    OP1 为 ARP 请求,2 为 ARP 应答
    

    ARP 报文结构的实现,结构体中的变量和上面图中的每个字段一一对应:

    struct arp_hdr
    {
        uint16_t hwtype;
        uint16_t protype;
        uint8_t hwsize;
        uint8_t prosize;
        uint16_t opcode;
        unsigned char data[];
    } __attribute__((packed));
    
    struct arp_ipv4
    {
        unsigned char smac[6];
        uint32_t sip;
        unsigned char dmac[6];
        uint32_t dip;
    } __attribute__((packed));
    

    ARP 数据帧的发送接口由 arp_request 函数实现

    int arp_request(uint32_t sip, uint32_t dip, struct netdev *netdev)
    {
        struct sk_buff *skb;
        struct arp_hdr *arp;
        struct arp_ipv4 *payload;
        int rc = 0;
        
        skb = arp_alloc_skb();
    
        if (!skb) return -1;
    
        skb->dev = netdev;	// 选择网卡
    
        payload = (struct arp_ipv4 *) skb_push(skb, ARP_DATA_LEN);	// 向前移动 arp_ipv4 大小的位置,也就是上图后四个字段,先解析这几个,现在 payload 相当于一个指向 arp_ipv4 的指针
    
        memcpy(payload->smac, netdev->hwaddr, netdev->addr_len);
        payload->sip = sip;
    
        memcpy(payload->dmac, broadcast_hw, netdev->addr_len);
        payload->dip = dip;
    	// 填充这 4 个字段
        
        arp = (struct arp_hdr *) skb_push(skb, ARP_DATA_LEN);
    	// 同样的道理,前移 arp_hdr 大小,现在的 arp 是指向 arp_hdr 的指针
        arp_dbg("req", arp);
        arp->opcode = htons(ARP_REQUEST);
        arp->hwtype = htons(ARP_ETHERNET);
        arp->protype = htons(ETH_P_IP);
        arp->hwsize = netdev->addr_len;
        arp->prosize = 4;
        // 同样进行填充
    
        arpdata_dbg("req", payload);
        payload->sip = htonl(payload->sip);
        payload->dip = htonl(payload->dip);
        // 做一下格式转换
    
        rc = netdev_transmit(skb, broadcast_hw, ETH_P_ARP);
        // 进一步构建以太网数据帧进行发送
        free_skb(skb);
        return rc;
    }
    

    上面是发送了 arp 请求,接着写一个接口来接收 arp 请求

    void arp_reply(struct sk_buff *skb, struct netdev *netdev)
    {
        struct arp_hdr *arphdr;
        struct arp_ipv4 *arpdata;
    
        arphdr = arp_hdr(skb);  // 获取报文数据
    
        skb_reserve(skb, ETH_HDR_LEN + ARP_HDR_LEN + ARP_DATA_LEN);		// 接收了请求头(并处理)
        skb_push(skb, ARP_HDR_LEN + ARP_DATA_LEN);	// 已经接收完了,可以跳过去了
    
        arpdata = (struct arp_ipv4 *) arphdr->data;
    
        memcpy(arpdata->dmac, arpdata->smac, 6);
        arpdata->dip = arpdata->sip;
    
        memcpy(arpdata->smac, netdev->hwaddr, 6);
        arpdata->sip = netdev->addr;
    
        arphdr->opcode = ARP_REPLY;
        
        arp_dbg("reply", arphdr);
        arphdr->opcode = htons(arphdr->opcode);
        arphdr->hwtype = htons(arphdr->hwtype);
        arphdr->protype = htons(arphdr->protype);
    
        arpdata_dbg("reply", arpdata);
        arpdata->sip = htonl(arpdata->sip);
        arpdata->dip = htonl(arpdata->dip);
        // 解析 arp 报文头,分别存放在 arphdr 和 arpdata 中
        
        skb->dev = netdev;	// 用指定的网卡发送
    
        netdev_transmit(skb, arpdata->dmac, ETH_P_ARP); // 收到之后回复一个相应
        free_skb(skb);
    }
    
  • 编写 Socket 结构

    socket 被翻译为套接字,实际上是应用层和传输层的一个抽象层,具体表现为应用层用户进程需要通过 socket 和 协议栈中的下层进行通讯和交互。

    image-20220322174908342

    在 linux 下一切皆文件,对 socket 的操作也是通过文件读写实现的。

    看一下套接字的格式(Linux 库中自带)

    通用套接字:

    struct sockaddr{
        sa_family_t sa_family;  /* 地址族.  16-bit*/
        char sa_data[14];   /* 具体的地址值 112-bit */
      };
    

    IPv4 套接字格式地址:

    struct sockaddr_in
      {
        sa_family_t sin_family; /* 地址族.  16-bit*/
        in_port_t sin_port;     /* 端口  16-bit*/
        struct in_addr sin_addr;
    
        /* 这里仅仅用作占位符,不做实际用处  */
        unsigned char sin_zero[8];
      };
    

    IPv6 套接字地址格式:

    struct sockaddr_in6
      {
        sa_family_t sin6_family; /* 地址族.  16-bit*/
        in_port_t sin6_port;  /* 传输端口号 # 16-bit */
        uint32_t sin6_flowinfo; /* IPv6流控信息 32-bit*/
        struct in6_addr sin6_addr;  /* IPv6地址128-bit */
        uint32_t sin6_scope_id; /* IPv6域ID 32-bit */
      };
    

    关于地址族,也就是在 socket 编程中 socket 函数的第一个参数:

    AF_LOCAL:本地通信
    AF_INET:IPv4地址
    AF_INET6:IPv6地址
    
  • IP 结构实现

    报文结构如下:

    image-20220323084543560

    ip 数据首部结构在 include/ip.h 中

    struct iphdr {
        uint8_t ihl : 4;	// 首部长度(最小是5)
        uint8_t version : 4;	// 版本号,分别对应 4 / 6
        uint8_t tos;	// 服务字段类型(最小延时、最大吞吐量、最高可靠性、最小费用)
        uint16_t len;	// ip 数据包的总字节数(数据链路层通常限制为 1500,IP 分片技术)
        uint16_t id;	// 发生分片时记录每个 ip 分片的序号
        uint16_t frag_offset; //标志该 ip 数据报在转发过程是否允许分片以及是否是最后一个分片。分片偏移量记录该分片 ip 数据报在整个数据报中的相对位置
        uint8_t ttl;	// 最多能被转发的次数,每转发一次减 1
        uint8_t proto;	// 此 ip 数据报中的数据来自哪一个上层协议
        uint16_t csum;	// 首部校验和
        uint32_t saddr;	// 本地主机 ip
        uint32_t daddr;	// 目标主机 up
        uint8_t data[];	// 非必须,共不同上层协议使用
    } __attribute__((packed));
    

    ip 数据报的发送接口实现(ip_output函数)

    int ip_output(struct sock *sk, struct sk_buff *skb)
    {
        struct rtentry *rt;
        struct iphdr *ihdr = ip_hdr(skb);
    
        rt = route_lookup(sk_daddr);	// 搜索路由表,根据目标 ip 选择合适的网卡(同一网段)
    
        if (!rt) {
            // TODO: dest_unreachable
            print_err("IP output route lookup fail\n");
            return -1;
        }
    
        skb->dev = rt->dev;	
        skb->rt = rt;
        // 把选中的网卡和路由记录在 sk_buff 结构体
    
        skb_push(skb, IP_HDR_LEN);	// 预留 ip 数据包首部
    
        ihdr->version = IPV4;
        ihdr->ihl = 0x05;
        ihdr->tos = 0;
        ihdr->len = skb->len;
        ihdr->id = ihdr->id;
        ihdr->frag_offset = 0x4000;
        ihdr->ttl = 64;
        ihdr->proto = skb->protocol;
        ihdr->saddr = skb->dev->addr;
        ihdr->daddr = sk->daddr;
        ihdr->csum = 0;
    	// 填充首部信息
        ip_dbg("out", ihdr);
    
        ihdr->len = htons(ihdr->len);
        ihdr->id = htons(ihdr->id);
        ihdr->daddr = htonl(ihdr->daddr);
        ihdr->saddr = htonl(ihdr->saddr);
        ihdr->csum = htons(ihdr->csum);
        ihdr->frag_offset = htons(ihdr->frag_offset);
    	//  做一下格式转换
        ip_send_check(ihdr);	// 计算校验和
    	
        return dst_neigh_output(skb);	// 发送!
    }
    
    

    发送的细节继续跟进 dst_neigh_output,这里参数只传了一个 skb,该结构体负责网络数据发送的全部过程。

    int dst_neigh_output(struct sk_buff *skb)
    {
        struct iphdr *iphdr = ip_hdr(skb);
        struct netdev *netdev = skb->dev;
        struct rtentry *rt = skb->rt;
        uint32_t daar = ntohl(iphdr->daddr);
        uint32_t saddr = ntohl(iphdr->saddr);
    
        uint8_t *dmac;
    
        if (rt->flags & RT_GATEWAY) {
            daddr = rt->gateway;
        }
    
        dmac = arp_get_hwaddr(daddr);	// 从 arp 缓存表中查询目标 ip 对应以太网地址
    
        if (dmac) {
            return netdev_transmit(skb, dmac, ETH_P_IP);
        } else {
            arp_request(saddr, daddr, netdev);
    	// 如果 arp 缓存表中有记录则直接发送,否则先发 arp 请求查询接口(广播)
             /* Inform upper layer that traffic was not sent, retry later */
            return -1;
        }
    }
    

    然后是 ip 数据接收端口 ip_rcv,这个函数在前面的接收数据流函数 netdev_receive 中,经过判断协议类型是 ip 还是 arp 后调用。

    int ip_rcv(struct sk_buff *skb)
    {
        struct iphdr *ih = iphdr(skb);	// 读取 ip 首部信息
        uint16_t csum = -1;
    
        if (ih->version != IPV4) {
            print_err("Datagram version was not IPv4\n");
            goto drop_pkt;
        }
    
        if (ih->ihl < 5) {
            print_err("IPv4 header length must be at least 5\n");
            goto drop_pkt;
        }
    
        if (ih->ttl == 0) {
            //TODO: Send ICMP error
            print_err("Time to live of datagram reached 0\n");
            goto drop_pkt;
        }
        
        csum = checksum(ih, ih->ihl * 4, 0);
    
        if (csum != 0) {
            // Invalid checksum, drop packet handling
            goto drop_pkt;
        }
    	// 一系列头部校验
        // TODO: Check fragmentation, possibly reassemble
    
        ip_init_pkt(ih);	// 字段进行小段转换
    
        ip_dbg("in", ih);
    
        switch (ip->proto) {	// 判断应用层协议类型,并转交数据包给对应处理逻辑
        case ICMPV4:
            icmpv4_incoming(skb);
            return 0;
        case IP_TCP:
            tcp_in(skb);
            return 0;
        default:
            print_err("Unknown IP header proto\n");
            goto drop_pkt;
        }
    
    drop_pkt:
        free_skb(skb);
        return 0;
    }
    
  • ICMP 包相关实现

    众所周知 ping 命令就是走的 icmp 协议(在基础的 icmp 格式后面增加了自己的格式),icmp 是和 tcp,udp 同一级的传输层协议,首先看一下报文结构,在 include/icmpv4.h 文件中

    struct icmp_v4 {
        uint8_t type;
        uint8_t code;
        uint16_t csum;
        uint8_t data[];
    } __attribute__((packed));
    

    ICMP 数据接收端口,是完成 ip_rcv 之后的可选分支之一:

    void icmpv4_incoming(struct sk_buff *skb)
    {
        struct iphdr *iphdr = ip_hdr(skb);
        struct icmp_v4 *icmp = (struct icmp_v4 *) ip_hdr->data;
    	// 获取 icmp 报文
        //TODO: Check csum
    
        switch (icmp->type) {	// 判断报文类型
        case ICMP_V4_ECHO:
            icmpv4_reply(skb);	// 回复
            return;
        case ICMP_V4_DST_UNREACHABLE:	// 不可达
            print_err("ICMPv4 received 'dst unreachable' code %d, "
                      "check your routes and firewall rules\n", icmp->code);
            goto drop_pkt;
        default:
            print_err("ICMPv4 did not match supported types\n");
            goto drop_pkt;
        }
    
    drop_pkt:
        free_skb(skb);
        return;
    }
    

    接收到 icmp 查询报文后,如果类型正确,会调用 icmpv4_reply 进行回复:

    void icmpv4_reply(struct sk_buff *skb)
    {
        struct iphdr *iphdr = ip_hdr(skb);	// 获取数据包首部
        struct icmp_v4 *icmp;
        struct sock sk;
        memset(&sk, 0, sizeof(struct sock));
    
        uint16_t icmp_len = iphdr->len - (iphdr->ihl * 4);	
        // ip 包总长度 - ip 首部长度 = icmp 包总长度
        
        skb_reserve(skb, ETH_HDR_LEN + IP_HDR_LEN + icmp_len);
        // sk_buff 指针后移至有效数据结束
        skb_push(skb, icmp_len);
        // sk_buff 前移 icmp_len 的长度,开始解析 icmp 包
    
        icmp = (struct icmp_v4 *)skb->data;
    
        icmp->type = ICMP_V4_REPLY;	// 应答类型
        icmp->csum = 0;		// ping 应答
        icmp->csum = checksum(icmp, icmp_len, 0);	// 修改校验数据
    
        skb->protocol = ICMPv4;
        sk.daddr = iphdr->saddr;
    
        ip_output(&sk, skb);	// 送到下一层的 ip 层进行发送
        free_skb(skb);
    }
    
  • TCP 包相关实现

    TCP 协议有自己的数据包格式(报文段),TCP 报文段封装在 IP 数据包中发送(uint8_t data[]),所以 TCP 报文段由 TCP 首部和 TCP 数据区组成,首部包含了连接建立与断开,数据确认,窗口大小通告,数据发送的相关标志和控制信息。

    image-20220323125710110

    具体实现在 include/tcp.h 中

    struct tcphdr {
        uint16_t sport;		// 源端口号
        uint16_t dport;		// 目的端口号
        uint32_t seq;
        // 是发送端到接收端的数据字节编号,值为当前报文段中第一个数据的字节序号,接收方先计算数据区长度,然后就能根据这个序号字段计算出最后一个数据的序号。
        // 当建立一个新连接的时候,SYN 初始化为 1,此时的序号字段是发送方随机选择的初始序号(ISN:Initial Sequence Number),之后发送方每发送数据的第一个字节序号为 ISN + 1
        uint32_t ack_seq;
        // 确认序号只有 ACK 标志为 1 的时候才有效,包含了本机所期望收到的下一个数据序号
        uint8_t rsvd : 4;	// 不使用
        uint8_t hl : 4;		// 首部长度、
        uint8_t fin : 1,	// 终止连接
                syn : 1,	// 发起连接,同步序号
                rst : 1,	// 连接复位
                psh : 1,	// 尽快交付(推送数据)
                ack : 1,	// 首部中的确认序号字段有效
                urg : 1,	// 首部中的紧急指针字段有效
                ece : 1,	// 收到拥塞通知
                cwr : 1;	// 通知发送方降低发送速率
        uint16_t win;	
        // 窗口大小字段,通知对方自己可用单独缓冲区大小(字节为单位),为 0 的时候阻止发送方发送数据,流量控制和拥塞控制的本质在于对发送窗口的合理调节
        uint16_t csum;
        uint16_t urp;	// 紧急数据时钟放在报文段数据最开始的地方,紧急指针定义出了紧急数据在数据区的结束处
        uint8_t data[];
    } __attribute__((packed));
    

    接着是 tcp 报文的发送,这个函数会在 tcp 建立连接的时候被多次调用(三次握手,write,read),在 src\tcp_output.c 中实现

    static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, uint32_t seq)
    {
        struct tcp_sock *tsk = tcp_sk(sk);
        struct tcb *tcb = &tsk->tcb;
        struct tcphdr *thdr = tcp_hdr(skb);
    
        /* No options were previously set */
        if (thdr->hl == 0) thdr->hl = TCP_DOFFSET;
    
        skb_push(skb, thdr->hl * 4);
    
        thdr->sport = sk->sport;	// 源端口号
        thdr->dport = sk->dport;	// 目的端口号
        thdr->seq = seq;			// 起始序号
        thdr->ack_seq = tcb->rcv_nxt;	// 通知对方期望接受的下一字节的序号(见三次握手部分)
        thdr->rsvd = 0;			// 保留位	
        thdr->win = tcb->rcv_wnd;	// 滑动窗口大小
        thdr->csum = 0;		// 校验和设置为 0
        thdr->urp = 0;		// 紧急指针设置为 0
    
        if (thdr->hl > 5) {
            tcp_write_options(tsk, thdr);
        }
    
        tcp_out_dbg(thdr, sk, skb);
    
        thdr->sport = htons(thdr->sport);
        thdr->dport = htons(thdr->dport);
        thdr->seq = htonl(thdr->seq);
        thdr->ack_seq = htonl(thdr->ack_seq);
        thdr->win = htons(thdr->win);
        thdr->csum = htons(thdr->csum);
        thdr->urp = htons(thdr->urp);
        // 格式转换
        thdr->csum = tcp_v4_checksum(skb, htonl(sk->saddr), htonl(sk->daddr));	// 设置校验和
        return ip_output(sk, skb);	// 发给 ip 层处理
    }
    

    tcp 接收端口 tcp_in函数(src\tcp.c),在 ip_rcv 中被调用,

    void tcp_in(struct sk_buff *skb)
    {
        struct sock *sk;
        struct iphdr *iph;
        struct tcphdr *th;
    
        iph = ip_hdr(skb);	// 读取 ip 首部信息
        th = (struct tcphdr*) iph->data;	// 从 ip 包的 data 区域读取 tcp 数据的信息
    
        tcp_init_segment(th, iph, skb);	// 大小端变换和计算应答序号(详见三次握手)
        
        sk = inet_lookup(skb, th->sport, th->dport);	// 查找管理该 tcp 连接的 sock 结构体(根据两个端口号可确定唯一连接)
    
        if (sk == NULL) {
            print_err("No TCP socket for sport %d dport %d\n",
                      th->sport, th->dport);
            free_skb(skb);
            return;
        }
        socket_wr_acquire(sk->sock);	// 获取该结构体的读写锁
    
        tcp_in_dbg(th, sk, skb);
        /* if (tcp_checksum(iph, th) != 0) { */
        /*     goto discard; */
        /* } */
        tcp_input_state(sk, th, skb);	// 根据当前 tcp 通信状态的变化,把整理好的报文递交给应用程序
    
        socket_release(sk->sock);
    }
    
  • 参考文献

posted @   moon_flower  阅读(866)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示