IP_PKTINFO选项

如下的套接口选项设置命令,用于开启SOL_IP层面的IP_PKTINFO选项,可获取接收报文的相关信息,也可在发送报文时指定报文的相关控制信息,以上信息以结构体in_pktinfo所表示。此选项仅针对数据报类型的套接口(如UDP),并且通过接口recvmsg和sendmsg结构实现。

    int enable = 1;
    setsockopt(s->s, SOL_IP, IP_PKTINFO, &enable, sizeof(enable));

结构体in_pktinfo如下,其中ipi_ifindex表示接收报文的接口索引;成员ipi_spec_dst表示报文的本地地址;而ipi_addr表示报文头部的目的地址。在使用sendmsg结构发送报文时,如果ipi_spec_dst字段不为零,在内核中查找路由时,将使用此指定地址为源地址进行查询。再者,如果ipi_ifindex不为零,在查找路由时,此索引所对应的接口的索引作为查找路由时的出接口索引。

    struct in_pktinfo {
        unsigned int   ipi_ifindex;  /* Interface index */
        struct in_addr ipi_spec_dst; /* Local address */
        struct in_addr ipi_addr;     /* Header Destination
                                        address */
    };

IP_PKTINFO选项控制

相关处理函数位于net/ipv4/ip_sockglue.c文件中,如下的do_ip_setsockopt处理SOL_IP层面的套接口设置选项,对于IP_PKTINFO选项,将inet_sock的成员cmsg_flags或上位标志IP_CMSG_PKTINFO。

static int do_ip_setsockopt(struct sock *sk, int level, int optname, char __user *optval, unsigned int optlen)
{
    struct inet_sock *inet = inet_sk(sk);

    switch (optname) {
    case IP_PKTINFO:
        if (val)
            inet->cmsg_flags |= IP_CMSG_PKTINFO;
        else
            inet->cmsg_flags &= ~IP_CMSG_PKTINFO;
        break;

函数do_ip_getsockopt用户获取SOL_IP层面的套接口选项状态,对于IP_PKTINFO,获取IP_CMSG_PKTINFO位是否为1。

static int do_ip_getsockopt(struct sock *sk, int level, int optname, char __user *optval, int __user *optlen, unsigned int flags)
{
    struct inet_sock *inet = inet_sk(sk);

    switch (optname) {
    case IP_PKTINFO:
        val = (inet->cmsg_flags & IP_CMSG_PKTINFO) != 0;
        break;

IP_PKTINFO信息

如下的UDP协议接收函数udp_recvmsg,如果inet_sock套接口cmsg_flags标志不为空,使用函数ip_cmsg_recv_offset添加控制信息。

int udp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int noblock, int flags, int *addr_len)
{
    struct inet_sock *inet = inet_sk(sk);
    ...
    if (inet->cmsg_flags)
        ip_cmsg_recv_offset(msg, sk, skb, sizeof(struct udphdr), off);

如果套接口设置了IP_CMSG_PKTINFO标志,将由函数ip_cmsg_recv_pktinfo添加控制信息。

void ip_cmsg_recv_offset(struct msghdr *msg, struct sock *sk, struct sk_buff *skb, int tlen, int offset)
{
    struct inet_sock *inet = inet_sk(sk);
    unsigned int flags = inet->cmsg_flags;

    /* Ordered by supposed usage frequency */
    if (flags & IP_CMSG_PKTINFO) {
        ip_cmsg_recv_pktinfo(msg, skb);

        flags &= ~IP_CMSG_PKTINFO;
        if (!flags)
            return;

如之前所述,ipi_addr中保存了报文IP头部的目的地址。结构体in_pktinfo中的其它两个字段保存在skb的回调cb字段,参见宏PKTINFO_SKB_CB。

static void ip_cmsg_recv_pktinfo(struct msghdr *msg, struct sk_buff *skb)
{
    struct in_pktinfo info = *PKTINFO_SKB_CB(skb);

    info.ipi_addr.s_addr = ip_hdr(skb)->daddr;

    put_cmsg(msg, SOL_IP, IP_PKTINFO, sizeof(info), &info);
}

宏PKTINFO_SKB_CB中的内容在UDP的接收路径中初始化,在函数udp_queue_rcv_one_skb中调用子函数ipv4_pktinfo_prepare实现。

static int udp_queue_rcv_one_skb(struct sock *sk, struct sk_buff *skb)
{
    ...
    ipv4_pktinfo_prepare(sk, skb);
    return __udp_queue_rcv_skb(sk, skb);

在函数ipv4_pktinfo_prepare中,skb的回调字段cb的空间,还是inet{6}_skb_parm的类型,由于其第一个成员为iif,与in_pktinfo结构的第一个成员ipi_ifindex相同,,在将cb做类型强转之后,in_pktinfo的成员ipi_ifindex就有值了。

如果ipi_ifindex为回环接口的索引值,将其更正为发送接口的索引值,参见函数inet_iif。另外对于l3mdev模块的从设备报文,如果路由缓存中的rt_iif有值,说明为环回报文,将ipi_ifindex索引跟新为路由缓存中保存的原始发送接口索引。

对于in_pktinfo结构的成员ipi_spec_dst,由函数fib_compute_spec_dst赋值,稍后进行介绍。

void ipv4_pktinfo_prepare(const struct sock *sk, struct sk_buff *skb)
{   
    struct in_pktinfo *pktinfo = PKTINFO_SKB_CB(skb);
    bool prepare = (inet_sk(sk)->cmsg_flags & IP_CMSG_PKTINFO) || ipv6_sk_rxinfo(sk);
    
    if (prepare && skb_rtable(skb)) {
        struct rtable *rt = skb_rtable(skb);
        bool l3slave = ipv4_l3mdev_skb(IPCB(skb)->flags);
        
        if (pktinfo->ipi_ifindex == LOOPBACK_IFINDEX)
            pktinfo->ipi_ifindex = inet_iif(skb);
        else if (l3slave && rt && rt->rt_iif)
            pktinfo->ipi_ifindex = rt->rt_iif;
        
        pktinfo->ipi_spec_dst.s_addr = fib_compute_spec_dst(skb);

对于ipi_ifindex索引等于环回接口LOOPBACK_IFINDEX的情况,如果路由缓存中的rt_iif有值,inet_iif函数返回此值作为原始发送接口索引,否者,使用skb接口中的skb_iif值,此值在接收路径的函数__netif_receive_skb_core中赋值。

static inline int inet_iif(const struct sk_buff *skb)
{    
    struct rtable *rt = skb_rtable(skb);

    if (rt && rt->rt_iif)
        return rt->rt_iif;
    return skb->skb_iif;

如下为函数fib_compute_spec_dst,对于路由缓存中仅有RTCF_LOCAL本地标志,而没有广播和多播标志的情况,返回报文IP头部中的目的地址,可见此时in_pktinfo结构中的ipi_spec_dst就和ipi_addr成员的值相同。

否则,如果路由缓存显示为广播或者多播属性,并且如果报文的源IP地址不是零网地址,使用此地址作为目的地址,执行路由查找,返回查找到的路由中的源地址(FIB_RES_PREFSRC)。其它情况下,使用函数inet_select_addr,也是以报文IP头中的源地址作为目的地址,查找相符的源地址。

__be32 fib_compute_spec_dst(struct sk_buff *skb)
{   
    struct net_device *dev = skb->dev;
    struct in_device *in_dev;

    rt = skb_rtable(skb); 
    if ((rt->rt_flags & (RTCF_BROADCAST | RTCF_MULTICAST | RTCF_LOCAL)) == RTCF_LOCAL)
        return ip_hdr(skb)->daddr;
    
    in_dev = __in_dev_get_rcu(dev);
    net = dev_net(dev);

    scope = RT_SCOPE_UNIVERSE;
    if (!ipv4_is_zeronet(ip_hdr(skb)->saddr)) {
        bool vmark = in_dev && IN_DEV_SRC_VMARK(in_dev);
        struct flowi4 fl4 = {
            .flowi4_iif = LOOPBACK_IFINDEX,
            .flowi4_oif = l3mdev_master_ifindex_rcu(dev),
            .daddr = ip_hdr(skb)->saddr,
            .flowi4_tos = RT_TOS(ip_hdr(skb)->tos),
            .flowi4_scope = scope,
            .flowi4_mark = vmark ? skb->mark : 0,
        };
        if (!fib_lookup(net, &fl4, &res, 0))
            return FIB_RES_PREFSRC(net, res);
    } else {
        scope = RT_SCOPE_LINK;
    }

    return inet_select_addr(dev, ip_hdr(skb)->saddr, scope);

至此,in_pktinfo结构中的信息都填充完成。

IP_PKTINFO控制信息

如下以UDP协议为例,在函数udp_sendmsg中,如果控制消息的长度不为零,首先处理UDP层的控制消息,如果其返回值大于零,表明需要进行IP层控制消息的处理,调用处理函数ip_cmsg_send。

int udp_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
{
    if (msg->msg_controllen) {
        err = udp_cmsg_send(sk, msg, &ipc.gso_size);
        if (err > 0)
            err = ip_cmsg_send(sk, msg, &ipc,
                       sk->sk_family == AF_INET6);
        if (unlikely(err < 0)) {
            kfree(ipc.opt);
            return err;
        }
        if (ipc.opt)
            free = 1;
        connected = 0;
    }

在函数ip_cmsg_send中,将in_pktinfo结构中的成员ipi_ifindex和ipi_spec_dst赋值给ipcm_cookie结构的成员ofi和addr变量。

int ip_cmsg_send(struct sock *sk, struct msghdr *msg, struct ipcm_cookie *ipc, bool allow_ipv6)
{
    struct cmsghdr *cmsg;
    struct net *net = sock_net(sk);

    for_each_cmsghdr(cmsg, msg) {
        switch (cmsg->cmsg_type) {
        case IP_PKTINFO:
        {
            struct in_pktinfo *info;
            if (cmsg->cmsg_len != CMSG_LEN(sizeof(struct in_pktinfo)))
                return -EINVAL;
            info = (struct in_pktinfo *)CMSG_DATA(cmsg);
            if (info->ipi_ifindex)
                ipc->oif = info->ipi_ifindex;
            ipc->addr = info->ipi_spec_dst.s_addr;
            break;
        }

在回过头来看函数udp_sendmsg的后续处理,对于flowi4结构类型变量fl4,其成员flowi4_oif和saddr,赋予了ipc中的oif和addr值,作为查询路由时使用的出接口和源IP地址。

    saddr = ipc.addr;

    if (!rt) {
        struct net *net = sock_net(sk);
        __u8 flow_flags = inet_sk_flowi_flags(sk);

        fl4 = &fl4_stack;
        flowi4_init_output(fl4, ipc.oif, sk->sk_mark, tos,
                   RT_SCOPE_UNIVERSE, sk->sk_protocol,
                   flow_flags,
                   faddr, saddr, dport, inet->inet_sport,
                   sk->sk_uid);

        security_sk_classify_flow(sk, flowi4_to_flowi(fl4));
        rt = ip_route_output_flow(net, fl4, sk);

如下为路由查询函数ip_route_output_key_hash_rcu,在flowi4_oif不为零的情况下,如果目的地址为多播或者广播、或者为协议IGMP时,如果为指定saddr,选择出口设备上的合适地址为源地址。另外,以上不成立,如果目的地址为多播,或者目的地址为0,都选择出口设备上的合适地址为源地址,但是两者使用的scope不同,对于目的地址为零的情况,报文发往本地,scope使用RT_SCOPE_HOST。

struct rtable *ip_route_output_key_hash_rcu(struct net *net, struct flowi4 *fl4, struct fib_result *res, const struct sk_buff *skb)
{
    struct net_device *dev_out = NULL;
    int orig_oif = fl4->flowi4_oif;

    if (fl4->flowi4_oif) {
        dev_out = dev_get_by_index_rcu(net, fl4->flowi4_oif);

        /* RACE: Check return value of inet_select_addr instead. */
        if (!(dev_out->flags & IFF_UP) || !__in_dev_get_rcu(dev_out)) {
            rth = ERR_PTR(-ENETUNREACH);
            goto out;
        }
        if (ipv4_is_local_multicast(fl4->daddr) || ipv4_is_lbcast(fl4->daddr) || fl4->flowi4_proto == IPPROTO_IGMP) {
            if (!fl4->saddr)
                fl4->saddr = inet_select_addr(dev_out, 0, RT_SCOPE_LINK);
            goto make_route;
        }
        if (!fl4->saddr) {
            if (ipv4_is_multicast(fl4->daddr))
                fl4->saddr = inet_select_addr(dev_out, 0, fl4->flowi4_scope);
            else if (!fl4->daddr)
                fl4->saddr = inet_select_addr(dev_out, 0, RT_SCOPE_HOST);
        }
    }

通过man 7 ip中可见其中介绍IP_PKTINFO是提到,如果指定了ipi_ifindex,其接口地址将覆盖指定的ipi_spec_dst所指定的地址。但是,以上路由函数中并没有见到此逻辑?

    If IP_PKTINFO is passed to sendmsg(2) and ipi_spec_dst is not zero, then it is used as the local source address for 
	the routing table lookup and  for  setting  up  IP  source  route options.  When ipi_ifindex is not zero, the primary 
	local address of the interface specified by the index overwrites ipi_spec_dst for the routing table lookup.

内核版本 5.0

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页