百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

Linux内核源码走读之IPv4及IPv6

liebian365 2024-11-26 06:01 3 浏览 0 评论

最近在看内核网络协议栈的代码,打算写几篇文章记录下。本文是关于IPv4及IPv6相关的内核源码走读,包括IPv4/IPv6的初始化,以及IP报文的接收和发送。

IPv4

IPv4报头

首先看下IPv4报头的定义,对应内核源码中的结构体是struct iphdr:

struct iphdr {
    __u8    ihl:4,  //header length, 以4字节为单位,最小为4,最大为15
        version:4;  //总是4
    __u8    tos;
    __be16  tot_len;  //包括报头在内的数据包总长度
    __be16  id;  //对于分段来说,所有分段的id值都必须相同
    __be16  frag_off;  //后13bit为分段的偏移量,以8Byte为单位
    __u8    ttl;  //存活时间,每个转发节点都会将ttl减1
    __u8    protocol;  //包所属的第四层协议
    __sum16 check;  //报头校验和
    __be32  saddr;  //源IP地址
    __be32  daddr;  //目的IP地址
    /*The options start here. */ //IP选项,可选
};

IPv4的初始化

//net/ipv4/af_inet.c

static int inet_init(void)
    proto_register(&tcp_prot, 1)  //所有注册的协议可以通过cat /proc/net/protocols查看
    proto_register(&udp_prot, 1)
    proto_register(&raw_prot, 1)
    proto_register(&ping_prot, 1)
    sock_register(&inet_family_ops)

    //注册各协议的接收处理函数,最终赋值到全局变量inet_protos[protocol]
    inet_add_protocol(&icmp_protocol, IPPROTO_ICMP)  
    inet_add_protocol(&udp_protocol, IPPROTO_UDP)
    inet_add_protocol(&tcp_protocol, IPPROTO_TCP)
    inet_add_protocol(&igmp_protocol, IPPROTO_IGMP)
    
    //注册各协议的socket interface接口
    for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
        inet_register_protosw(q);

    arp_init()  //arp模块初始化
        dev_add_pack(&arp_packet_type)  //注册ETH_P_ARP=0x0806类型的处理函数
        arp_proc_init()  //cat /proc/net/arp, 查看arp表项
        register_netdevice_notifier(&arp_netdev_notifier)  //注册netdevice事件监听
    ip_init()
    tcp_init()
    udp_init()
    ping_init()
    icmp_init()
        icmp_sk_init(struct net *net)
            //每CPU注册一个ICMP RAW socket,用于处理接收的ICMP报文
            inet_ctl_sock_create
    ip_mr_init  //组播路由初始化

    //注册IP协议ETH_P_IP=0x0800接受处理函数ip_rcv,即注册IP报文接收入口函数
    dev_add_pack(&ip_packet_type)

这里展开看一下dev_add_pack函数

void dev_add_pack(struct packet_type *pt)
    struct list_head *head = ptype_head(pt)
         return &ptype_base[ntohs(pt->type)]  //ptype_base是一个全局变量数组,记录每个协议的处理函数
    list_add_rcu(&pt->list, head)  //将处理函数pt赋值到全局变量ptype_base中

接收IPv4数据包

Linux网卡驱动接收包有两种方式,NAPI和非NAPI。现在新的网卡驱动一般采用NAPI方式。 网卡驱动在通过接收中断、软中断等一些列处理后,最终调用napi_gro_receive将数据包上报到协议栈处理。

非NAPI方式,最终调用netif_rx。这里跟一下从napi_gro_receive开始的收包流程。

gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
    napi_skb_finish(dev_gro_receive(napi, skb), skb);
        netif_receive_skb_internal
            __netif_receive_skb
                __netif_receive_skb_core

__netif_receive_skb_core可以认为是内核协议栈处理接收包的起点,下面跟一下这个函数。

static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
    //ptype_all是所有包类型的接收处理,对应tcpdump、raw socket等处理
    list_for_each_entry_rcu(ptype, &ptype_all, list)
        if (pt_prev)
            ret = deliver_skb(skb, pt_prev, orig_dev);
        pt_prev = ptype;

    //如果这个设备有注册rx_handler,通过接口netdev_rx_handler_register注册
    //则将包交给注册的rx_handler处理。例如加入网桥的接口会被注册rx_handler
    rx_handler = rcu_dereference(skb->dev->rx_handler);
    if (rx_handler)
        rx_handler(&skb)

    //交给ptype_base里注册的对应协议类型的处理函数
    deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
                &ptype_base[ntohs(type) & PTYPE_HASH_MASK]);

ptype_base里的对象是通过dev_add_pack接口注册的,在上节IPv4初始化里,我们知道IPv4协议注册的对象是ip_packet_type

static struct packet_type ip_packet_type __read_mostly = {
    .type = cpu_to_be16(ETH_P_IP),
    .func = ip_rcv, //IP协议报文的入口
};

接下来跟踪ip_rcv源码。

int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
    iph = ip_hdr(skb);
    if (iph->ihl < 5 || iph->version != 4) //如果包长度小于20或版本不是4,则报头不合法
        goto inhdr_error;
    if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl))) //报头校验和校验
        goto csum_error;

    //netfilter的第一个钩子挂载点,NF_INET_PRE_ROUTING
    //如果没被netfilter过滤,最终调用ip_rcv_finish
    return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
               net, NULL, skb, dev, NULL,
               ip_rcv_finish);

static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)

    if (!skb_valid_dst(skb))
        //在路由选择子系统进行查找
        err = ip_route_input_noref(skb, iph->daddr, iph->saddr,iph->tos, dev);
            //ip_route_input_noref中如果判断此IP包是发给本地,
            //则skb->_skb_refdst的input函数赋值为ip_local_deliver
            //如果需要转发,则iput函数赋值为ip_forward

    if (iph->ihl > 5 && ip_rcv_options(skb, dev)) //ip报头的选项处理
        goto drop;

    //调用路由选择子系统查找到的input函数
    return dst_input(skb);
        skb_dst(skb)->input(skb) //即调用skb->_skb_refdst对象的input函数

//这里看一下struct dst_entry,即路由查找的结果
struct dst_entry {
    ...
    int (*input)(struct sk_buff *); //路由查找后的接收处理函数,发给本机的包对应函数为ip_local_deliver
    int (*output)(struct net *net, struct sock *sk, struct sk_buff *skb);

    ...
}

继续看一下IP包发给本地时的处理函数ip_local_deliver

int ip_local_deliver(struct sk_buff *skb)
    if (ip_is_fragment(ip_hdr(skb)))
        ip_defrag //如果是分片报文,则交给解分片函数处理

    //netfilter的第二个钩子挂载点NF_INET_LOCAL_IN
    return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
               net, NULL, skb, skb->dev, NULL,
               ip_local_deliver_finish);

static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
    //先查并发给raw socket
    raw = raw_local_deliver(skb, protocol);

    ipprot = rcu_dereference(inet_protos[protocol]);  //inet_protos: 各协议注册的处理函数
    if (ipprot)
        ret = ipprot->handler(skb);  //调用注册的协议处理函数,通过接口inet_add_protocol注册
                                     //比如IPPROTO_ICMP的处理函数icmp_rcv,IPPROTO_TCP的是tcp_v4_rcv

发送IPv4数据包

IPv4为L4层提供将数据包发到L2层的接口和功能。

从L4发送IPv4数据包的主要方法有两个,一个是方法ip_queue_xmit(),由TCPv4使用;一个是ip_append_data(),由UDPv4和ICMPv4使用。

先看方法ip_queue_xmit()

int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
    struct rtable *rt;
    //在路由子系统中查找路由
    rt = ip_route_output_ports
        ip_route_output_flow
            __ip_route_output_key
                ip_route_output_key_hash
                    ip_route_output_key_hash_rcu(net, fl4, &res, skb);
                        fib_lookup(net, fl4, res, 0);
    skb_dst_set_noref(skb, &rt->dst);
    ... //此处是一些填写ip header的逻辑
    res = ip_local_out(net, sk, skb);
        dst_output(net, sk, skb)
            skb_dst(skb)->output(net, sk, skb)  //一般地,这里的output是ip_output

//继续看下ip_output
int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
    //netfilter的钩子挂载点NF_INET_POST_ROUTING
    NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
                net, sk, skb, NULL, dev,
                ip_finish_output,
                !(IPCB(skb)->flags & IPSKB_REROUTED));

//ip_finish_output
static int ip_finish_output(struct net *net, struct sock *sk, struct sk_buff *skb)
    ip_finish_output2
        //从字面上理解,找到目的邻居,然后发给邻居
        neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
        res = neigh_output(neigh, skb);
            dev_queue_xmit //最终交给网卡驱动

再看方法ip_append_data

int ip_append_data(struct sock *sk, struct flowi4 *fl4,
           int getfrag(void *from, char *to, int offset, int len,
                   int odd, struct sk_buff *skb),
           void *from, int length, int transhdrlen,
           struct ipcm_cookie *ipc, struct rtable **rtp,
           unsigned int flags)
    __ip_append_data
        //此函数很长,此处略过。

转发

在前面接收IPv4数据包中讲到,接收的数据包经过路由查找后,如果是发给本机的,则走到ip_local_deliver。 如果是要转发,则走到ip_forward。下面看下ip_forward的代码。

int ip_forward(struct sk_buff *skb)
    //netfilter挂载点 NF_NET_FORWARD
    return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD,
               net, NULL, skb, skb->dev, rt->dst.dev,
               ip_forward_finish);

static int ip_forward_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
    dst_output(net, sk, skb)
    //dst_output在上节讲到过,最终会调用到dev_queue_xmit交给网卡驱动

关于IPv4,还有一些主题,比如接收组播数据包、IP选项、分段等,后面有时间再补充。

IPv4报文接收和发送的流程图如下:

IPv6

IPv6地址

学习IPv6之前,先看下IPv6地址。IPv6地址长度为128bit,由8部分组成,每部分16bit。

IPv6地址的写法为: xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx。

如果一部分、或连续的几部分都为0,则可用::表示。

在IPv6中,需要用到地址前缀,前缀相当于IPv4子网掩码,用/n表示。 比如2001:da7::/32,表示开头32bit为2001:0da7的所有IPv6地址。

一些特殊的IPv6地址:

  • 每个接口都必须至少有一个链路本地单播地址。路由器不得转发此地址的包。地址前缀为fe80::/64
  • 全局单播地址的通用格式如下:n位全局路由选择前缀,m位子网ID,余下的为接口ID
  • ::1为环回地址
  • 全0(0:0:0:0:0:0:0:0)地址称为未指定地址,用于DAD(重复地址检测)
  • 映射IPv4的IPv6地址,前80位为0,接下来16位为1,余下32位为IPv4地址,例如:::ffff:192.0.2.128

在Linux中,IPv6地址用in6_addr表示。

struct in6_addr {
    union {
        __u8        u6_addr8[16]; //用union定义了3种形式的128bit长度
        __be16      u6_addr16[8];
        __be32      u6_addr32[4];
    } in6_u;
#define s6_addr         in6_u.u6_addr8
#define s6_addr16       in6_u.u6_addr16
#define s6_addr32       in6_u.u6_addr32
};

IPv6报头

IPv6报头在Linux中的结构体是struct ipv6hdr:

struct ipv6hdr {
    __u8            priority:4, //流量优先级
                    version:4; //版本号,总是6
    __u8            flow_lbl[3]; //流标签
    __be16          payload_len; //数据包的长度,不包含包头
    __u8            nexthdr; //扩展报头或者上层协议编号
    __u8            hop_limit; //相当于ttl

    struct  in6_addr    saddr; //128bit源地址
    struct  in6_addr    daddr; //128bit目的地址
};

IPv6扩展报头: IPv6报头的nexthdr字段,指出下一个报头的编号。没有扩展报头或最后一个扩展报头,指示上层协议。

IPv6初始化

inet6_init执行各种IPv6的初始化工作,位于net/ipv6/af_inet6.c

static int __init inet6_init(void)
    //和IPv4初始化类似,一堆协议注册。没跟到这里注册的协议后面怎么用。
    proto_register(&tcpv6_prot, 1)
    proto_register(&udpv6_prot, 1)
    proto_register(&udplitev6_prot, 1)
    proto_register(&rawv6_prot, 1)
    proto_register(&pingv6_prot, 1)

    rawv6_init()
    sock_register(&inet6_family_ops)
    inet6_net_init
    ip6_mr_init
    icmpv6_init
        icmpv6_sk_init
        //注册ICMPv6的接收处理函数icmpv6_rcv
        inet6_add_protocol(&icmpv6_protocol, IPPROTO_ICMPV6)
    ndisc_init()
    igmp6_init
    ipv6_netfilter_init
    ip6_route_init
    ipv6_frag_init
        inet6_add_protocol(&frag_protocol, IPPROTO_FRAGMENT)
    udpv6_init
        inet6_add_protocol(&udpv6_protocol, IPPROTO_UDP)
    tcpv6_init
        inet6_add_protocol(&tcpv6_protocol, IPPROTO_TCP)
    ipv6_packet_init()
        //注册IPv6协议的接收处理函数 ipv6_rcv
        //ETH_P_IPV6 = 0x86DD
        dev_add_pack(&ipv6_packet_type);

接收IPv6数据包

IPv6数据包的主接收方法为ipv6_rcv(),看下这个函数源码

int ipv6_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
    ... //此处省略一系列校验和检查
    //netfilter挂载点NF_INET_PRE_ROUTING,最终调用ip6_rcv_finish
    return NF_HOOK(NFPROTO_IPV6, NF_INET_PRE_ROUTING,
               net, NULL, skb, dev, NULL,
               ip6_rcv_finish);

int ip6_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
    //路由查找
    ip6_route_input(skb);
        ip6_route_input_lookup
            fib6_rule_lookup

    return dst_input(skb);
        skb_dst(skb)->input(skb); //调用路由查找后的input函数
        //如果是给当前主机的包input为ip6_input
        //如果需要转发input为ip6_forward
        //如果数据包目的地址为组播input为ip6_mc_input

这里看下本地投递的情形,此时skb_dst(skb)->input函数为ip6_input

int ip6_input(struct sk_buff *skb)
    //netfilter挂载点NF_INET_LOCAL_IN
    return NF_HOOK(NFPROTO_IPV6, NF_INET_LOCAL_IN,
               dev_net(skb->dev), NULL, skb, skb->dev, NULL,
               ip6_input_finish);

static int ip6_input_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
    raw6_local_deliver //先投递给原始套接字
    ipprot = rcu_dereference(inet6_protos[nexthdr])
    if (ipprot) 
        ipprot->handler(skb) //调用在IPv6初始化时注册的协议处理函数

发送IPv6数据包

IPv6数据包的发送路径与IPv4很像。IPv6中也有两个发送IPv6数据包的主方法: 一个是ip6_xmit,由TCP、SCTP等使用;另一个是ip6_append_data,有UDP和RAW套接字使用。

最终的调用路径为:ip6_local_out->ip6_output->ip6_finish_output->交给网卡驱动。

IPv6报文接收和发送的流程图如下:

相关推荐

快递查询教程,批量查询物流,一键管理快递

作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...

一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递

对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...

快递查询单号查询,怎么查物流到哪了

输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...

3分钟查询物流,教你一键批量查询全部物流信息

很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...

快递单号查询,一次性查询全部物流信息

现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...

快递查询工具,批量查询多个快递快递单号的物流状态、签收时间

最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...

快递查询软件,自动识别查询快递单号查询方法

当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...

教你怎样查询快递查询单号并保存物流信息

商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...

简单几步骤查询所有快递物流信息

在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...

物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号

最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...

连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息

快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...

快递查询教程,快递单号查询,筛选更新量为1的单号

最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...

掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析

在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...

从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息

在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...

物流单号查询,在哪里查询快递

如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...

取消回复欢迎 发表评论: