如下的套接口选项设置命令,用于开启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