Linux内核传输层UDP源码分析

一、用户数据包协议(UDP)

1.UDP数据报头

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

2.UDP初始化操作

定义对象udp_protocol(net_protocol对象)并使用方法inet_add_protocol()来添加它,具体源码如下:

  1. udp_protocol 结构体分析(协议注册)
cpp 复制代码
static struct net_protocol udp_protocol = {
    .early_demux = udp_v4_early_demux,    // 早期解复用函数,处理初始阶段的数据包
    .early_demux_handler = udp_v4_early_demux, // 早期解复用处理器
    .handler = udp_rcv,                    // UDP 数据包接收处理函数,处理完整的 UDP 数据接收
    .err_handler = udp_err,                // 错误处理函数,处理 UDP 传输中的错误
    .no_policy = 1,                        // 标记是否忽略策略检查
    .netns_ok = 1                          // 标记是否支持网络命名空间
};
  • 作用 :这是内核中 UDP 协议的注册实现。通过定义 net_protocol 类型的 udp_protocol,向内核网络子系统注册 UDP 协议的处理函数。内核通过 inet_add_protocol() 方法将其加入协议处理链,使内核能够识别和处理 UDP 数据包。

2.inet_init_net 初始化(端口范围等配置)

  • 作用 :初始化网络命名空间内的 IPv4 相关参数。例如设置本地端口分配范围(ip_local_ports),限制程序动态申请端口的范围;配置 ping 套接字的用户组权限(ping_group_range),控制哪些用户组可创建 ping 套接字。

3.发送UDP数据包udp_sendmsg(...)

从UDP用户空间套接字中发送数据,可以使用系统调用send()、sendto()、sendmsg()和write()。这些系统调用最终都会由内核中的方法udp_sendmsg()来处理。以下是这个函数的流程图:

1. 函数参数

  • struct sock *sk:指向套接字的指针,该套接字表示当前进行 UDP 数据发送操作所使用的套接字对象,其中包含了套接字的各种状态信息和配置。
  • struct msghdr *msg:指向 msghdr 结构体的指针,这个结构体包含了要发送的数据以及目标地址等信息,例如数据缓冲区、目标地址结构体等。
  • size_t len:表示要发送的数据的长度。

2. 函数功能

udp_sendmsg 函数是 Linux 内核中用于通过 UDP 套接字发送数据的核心函数。它会对传入的消息进行一系列的检查和处理,包括验证目标地址、处理控制消息、查找路由表等操作,最终将数据封装成 UDP 数据包并发送出去。如果在发送过程中遇到错误,会返回相应的错误码;如果发送成功,则返回实际发送的数据长度。

3. 重要部分及流程讲解

3.1 基本参数检查和初始化
cpp 复制代码
if (len > 0xFFFF)
    return -EMSGSIZE;

if (msg->msg_flags & MSG_OOB)
    return -EOPNOTSUPP;

getfrag = is_udplite ? udplite_getfrag : ip_generic_getfrag;
  • 首先检查要发送的数据长度是否超过了 UDP 数据包的最大长度(0xFFFF),如果超过则返回 EMSGSIZE 错误。
  • 接着检查消息标志中是否包含 MSG_OOB(带外数据),由于 UDP 不支持带外数据,所以如果包含则返回 EOPNOTSUPP 错误。
  • 根据套接字是否为 UDP-Lite 类型,选择不同的数据获取函数。
3.2 处理已 corked 的套接字
cpp 复制代码
fl4 = &inet->cork.fl.u.ip4;
if (up->pending) {
    lock_sock(sk);
    if (likely(up->pending)) {
        if (unlikely(up->pending != AF_INET)) {
            release_sock(sk);
            return -EINVAL;
        }
        goto do_append_data;
    }
    release_sock(sk);
}
  • 检查套接字是否已经处于 corked 状态(即有未发送的数据包)。如果是,则加锁并进一步检查状态是否正确,若不正确则返回 EINVAL 错误,若正确则跳转到 do_append_data 标签处继续处理。
3.3 获取并验证目标地址
cpp 复制代码
if (usin) {
    if (msg->msg_namelen < sizeof(*usin))
        return -EINVAL;
    if (usin->sin_family != AF_INET) {
        if (usin->sin_family != AF_UNSPEC)
            return -EAFNOSUPPORT;
    }

    daddr = usin->sin_addr.s_addr;
    dport = usin->sin_port;
    if (dport == 0)
        return -EINVAL;
} else {
    if (sk->sk_state != TCP_ESTABLISHED)
        return -EDESTADDRREQ;
    daddr = inet->inet_daddr;
    dport = inet->inet_dport;
    connected = 1;
}
  • 如果 msg 中包含目标地址信息,则检查地址长度和地址族是否正确,获取目标 IP 地址和端口号,并验证端口号是否有效。
  • 如果 msg 中不包含目标地址信息,则检查套接字是否已经连接,如果未连接则返回 EDESTADDRREQ 错误,否则使用套接字中保存的目标地址和端口号,并标记为已连接。
3.4 处理控制消息
cpp 复制代码
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;
}
3.5 查找路由表
cpp 复制代码
if (connected)
    rt = (struct rtable *)sk_dst_check(sk, 0);

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, ipc.sockc.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);
    if (IS_ERR(rt)) {
        err = PTR_ERR(rt);
        rt = NULL;
        if (err == -ENETUNREACH)
            IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES);
        goto out;
    }

    err = -EACCES;
    if ((rt->rt_flags & RTCF_BROADCAST) &&
        !sock_flag(sk, SOCK_BROADCAST))
        goto out;
    if (connected)
        sk_dst_set(sk, dst_clone(&rt->dst));
}
  • 如果套接字已经连接,则检查缓存的路由信息是否可用。
  • 如果没有可用的路由信息,则初始化 flowi4 结构体,进行安全分类,并调用 ip_route_output_flow 函数查找路由表。如果查找失败,则根据错误码进行相应处理,如增加统计信息并跳转到 out 标签处。
  • 如果路由表中包含广播标志,但套接字不允许广播,则返回 EACCES 错误。
3.6 发送数据
cpp 复制代码
if (!corkreq) {
    struct inet_cork cork;

    skb = ip_make_skb(sk, fl4, getfrag, msg, ulen,
                      sizeof(struct udphdr), &ipc, &rt,
                      &cork, msg->msg_flags);
    err = PTR_ERR(skb);
    if (!IS_ERR_OR_NULL(skb))
        err = udp_send_skb(skb, fl4, &cork);
    goto out;
}

lock_sock(sk);
if (unlikely(up->pending)) {
    release_sock(sk);
    net_dbg_ratelimited("socket already corked\n");
    err = -EINVAL;
    goto out;
}
fl4 = &inet->cork.fl.u.ip4;
fl4->daddr = daddr;
fl4->saddr = saddr;
fl4->fl4_dport = dport;
fl4->fl4_sport = inet->inet_sport;
up->pending = AF_INET;

do_append_data:
up->len += ulen;
err = ip_append_data(sk, fl4, getfrag, msg, ulen,
                     sizeof(struct udphdr), &ipc, &rt,
                     corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);
if (err)
    udp_flush_pending_frames(sk);
else if (!corkreq)
    err = udp_push_pending_frames(sk);
else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))
    up->pending = 0;
release_sock(sk);
  • 如果不需要 cork 操作(即 corkreqfalse),则调用 ip_make_skb 函数创建一个 skb(套接字缓冲区),并调用 udp_send_skb 函数发送数据。
  • 如果需要 cork 操作,则加锁并检查套接字状态,更新相关信息,然后调用 ip_append_data 函数将数据追加到缓冲区中。如果追加过程中出现错误,则调用 udp_flush_pending_frames 函数清空缓冲区;如果不需要 cork 操作,则调用 udp_push_pending_frames 函数将缓冲区中的数据发送出去。
3.7 清理资源并返回结果
cpp 复制代码
out:
ip_rt_put(rt);
out_free:
if (free)
    kfree(ipc.opt);
if (!err)
    return len;
if (err == -ENOBUFS || test_bit(SOCK_NOSPACE, &sk->sk_socket->flags)) {
    UDP_INC_STATS(sock_net(sk),
                  UDP_MIB_SNDBUFERRORS, is_udplite);
}
return err;
  • 释放路由表资源和控制消息相关的内存。
  • 如果没有错误发生,则返回实际发送的数据长度;如果出现错误,则根据错误码进行相应的统计更新并返回错误码。

如果 msg 中包含控制消息,则调用 udp_cmsg_sendip_cmsg_send 函数处理这些控制消息。如果处理过程中出现错误,则释放相关资源并返回错误码。

4.接收来自网络层的UDP数据包udp_rcv(...)

方法udp_rcv()是负责接收来自网络层的UDP数据包的主要处理程序,函数流程如下:

1. 函数参数和功能

函数参数
  • struct sk_buff *skb:指向套接字缓冲区的指针,其中包含接收到的 UDP 数据包。
  • struct udp_table *udptable:指向 UDP 表的指针,该表用于存储 UDP 套接字的相关信息。
  • int proto:协议类型,通常为 IPPROTO_UDP
函数功能

udp_rcv 函数是一个简单的包装函数,它直接调用 __udp4_lib_rcv 函数来处理接收到的 UDP 数据包。__udp4_lib_rcv 函数是处理 UDP 数据包接收的核心函数,它会对数据包进行一系列的验证和处理,包括检查数据包长度、校验和,查找对应的套接字,将数据包传递给相应的套接字进行处理,或者在找不到合适套接字时发送 ICMP 错误消息。

2. 重要部分及流程讲解

2.1 基本参数检查和初始化
cpp 复制代码
if (!pskb_may_pull(skb, sizeof(struct udphdr)))
    goto drop;		/* No space for header. */

uh   = udp_hdr(skb);
ulen = ntohs(uh->len);
saddr = ip_hdr(skb)->saddr;
daddr = ip_hdr(skb)->daddr;
  • 首先检查套接字缓冲区中是否有足够的空间来存储 UDP 头部,如果没有则跳转到 drop 标签处丢弃数据包。
  • 提取 UDP 头部信息,包括 UDP 数据包长度 ulen,源 IP 地址 saddr 和目的 IP 地址 daddr
2.2 数据包长度验证
cpp 复制代码
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);
}
  • 检查 UDP 头部中记录的数据包长度 ulen 是否超过了实际接收到的数据包长度 skb->len,如果超过则跳转到 short_packet 标签处处理。
  • 如果协议类型为 IPPROTO_UDP,还需要检查 ulen 是否小于 UDP 头部长度,或者调用 pskb_trim_rcsum 函数对数据包进行裁剪和校验和更新,如果出现问题则跳转到 short_packet 标签处。
2.3 校验和初始化
cpp 复制代码
if (udp4_csum_init(skb, uh, proto))
    goto csum_error;

调用 udp4_csum_init 函数对 UDP 数据包的校验和进行初始化,如果初始化失败则跳转到 csum_error 标签处处理。

2.4 查找套接字并处理
cpp 复制代码
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;
}
  • 尝试从套接字缓冲区中获取关联的套接字 sk
  • 如果获取到了套接字,检查接收目标地址是否与套接字中的记录一致,如果不一致则更新。
  • 调用 udp_unicast_rcv_skb 函数将数据包传递给该套接字进行处理,并返回处理结果。
2.5 处理广播或多播数据包
cpp 复制代码
if (rt->rt_flags & (RTCF_BROADCAST|RTCF_MULTICAST))
    return __udp4_lib_mcast_deliver(net, skb, uh,
                    saddr, daddr, udptable, proto);

如果路由表项中包含广播或多播标志,则调用 __udp4_lib_mcast_deliver 函数处理广播或多播数据包。

2.6 再次查找套接字
cpp 复制代码
sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);
if (sk)
    return udp_unicast_rcv_skb(sk, skb, uh);

如果之前没有找到关联的套接字,再次调用 __udp4_lib_lookup_skb 函数根据源端口和目的端口在 UDP 表中查找合适的套接字。如果找到则将数据包传递给该套接字进行处理并返回结果,

这个过程通常包含以下步骤:

  • 把接收到的数据添加到套接字的接收缓冲区。
  • 若应用程序正在阻塞等待数据,就会唤醒该应用程序,让其从接收缓冲区读取数据。
  • 若应用程序采用的是非阻塞模式,就会在后续的轮询或事件通知中得知有新数据到达。
2.7 策略检查和校验和检查
cpp 复制代码
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_lib_checksum_complete 函数检查 UDP 数据包的校验和,如果校验和错误则跳转到 csum_error 标签处处理。
2.8 发送 ICMP 错误消息
cpp 复制代码
__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;

如果没有找到合适的套接字,增加相应的统计信息,调用 icmp_send 函数发送 ICMP 目的不可达(端口不可达)消息,然后释放套接字缓冲区并返回 0。

2.9 错误处理
cpp 复制代码
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;
  • short_packet 标签处:记录数据包长度过短的调试信息,然后跳转到 drop 标签处。
  • csum_error 标签处:记录校验和错误的调试信息,增加校验和错误的统计信息,然后跳转到 drop 标签处。
  • drop 标签处:增加接收错误的统计信息,释放套接字缓冲区并返回 0。

调用 xfrm4_policy_check 函数进行安全策略检查,如果检查不通过则跳转到 drop 标签处丢弃数据包。

5.UDP使用流程

1. 用户层调用(sendto)

  • 用户通过 socket () 创建 UDP 套接字,调用 sendto () 发送数据报
  • 参数包含目标 IP (47.95.193.211)、端口 (默认可能未指定,需填充)
  • 内核进入 inet_sendmsg () 处理,分配 skb_buff(创建位置:传输层)

2. 传输层处理(UDP 层)

  • 创建 sk_buff 数据结构(skb->dev = 网络设备指针)
  • 填充 UDP 头(源端口 8888,目标端口待确认)
  • 计算校验和(可选,由 net.core.udp_checksum 内核参数控制)
  • skb->protocol = htons (ETH_P_IP),标识上层协议

3. 网络层处理(IP 层)

  • 调用 ip_queue_xmit () 进行路由查找
  • 路由表查询:
    • 使用 FIB 表(fib_rules)查找最佳路由
    • 确定输出设备(eth0/wlan0 等)
    • 下一跳地址(可能是网关地址,若目标不在同一子网)
  • 填充 IP 头:
    • 源 IP (192.168.186.138),目标 IP (47.95.193.211)
    • TTL、协议号 (17)、校验和
  • 检查是否需要分片(根据 MTU)

4. 链路层处理

  • 调用 dev_queue_xmit () 进入链路层
  • ARP 表查询:
    • 使用 neigh_table 结构查找 arp_cache
    • 若下一跳是网关,查询网关 MAC
    • 若目标在同一子网,直接查目标 MAC
    • 若 ARP 缓存缺失,触发 ARP 请求
  • 填充链路层头(以太网为例):
    • 源 MAC(本地网卡 MAC)
    • 目标 MAC(下一跳或目标 MAC)
    • 类型字段 0x0800(IP 协议)

5. 物理层传输

  • sk_buff 通过 net_device_ops->ndo_start_xmit () 发送
  • 物理层将二进制数据转换为电信号 / 光信号传输

接收流程简要说明:

  1. 物理层接收信号并转为二进制数据
  2. 链路层剥离帧头,检查 CRC 校验
  3. IP 层校验和验证,路由表查找输入接口
  4. UDP 层校验和验证,端口号匹配
  5. 用户层通过 recvfrom () 接收数据

关键数据结构说明:

  • sk_buff:贯穿各层,包含协议头、数据负载、设备指针等
  • fib_rules:路由规则表,用于确定输出路径
  • arp_cache:基于 neigh_table 的邻居缓存,存储 IP-MAC 映射
  • net_device:网络设备结构体,包含发送 / 接收函数指针
相关推荐
hahala23333 分钟前
hosts 文件 VS DNS 解析
网络协议
用户2587141932636 分钟前
深入浅出--Linux基础命令知识(总结,配图文解释)
linux·ubuntu
风正豪33 分钟前
如何向 Linux 中加入一个 IO 扩展芯片
linux·运维·单片机
白帽少女安琪拉43 分钟前
24.pocsuite3:开源的远程漏洞测试框架
网络·网络安全
如果是君1 小时前
Ubuntu20.04安装运行DynaSLAM
linux·python·深度学习·神经网络·ubuntu
Dream Algorithm2 小时前
DICT领域有哪些重要的技术标准和规范?
网络·物联网·边缘计算
霸气的哦尼酱2 小时前
同一子网通信
网络·智能路由器
对你无可奈何2 小时前
高可用环境下Nginx服务管理脚本优化实践
linux·运维·nginx
go_to_hacker2 小时前
奇安信二面
网络·web安全·网络安全·渗透测试·代码审计·春招
无聊的烤苕皮3 小时前
RHCE(RHCSA复习:npm、dnf、源码安装实验)
linux·npm·云计算·dnf·rhcsa