原文:Linux kernel networking

协议:CC BY-NC-SA 4.0

十、IPsec

第九章讲述了 netfilter 子系统及其内核实现。本章讨论互联网协议安全(IPsec)子系统。IPsec 是一组协议,通过对通信会话中的每个 IP 数据包进行身份验证和加密来保护 IP 流量。大多数安全服务是由两个主要的 IPsec 协议提供的:身份验证报头(AH)协议和封装安全负载(ESP)协议。此外,IPsec 还提供了防止尝试窃听和再次发送数据包(重放攻击)的保护。根据 IPv6 规范,IPsec 是强制性的,而在 IPv4 中是可选的。然而,包括 Linux 在内的大多数现代操作系统都支持 IPv4 和 IPv6 中的 IPsec。第一个 IPsec 协议定义于 1995 年(RFC 1825–1829)。1998 年,这些 RFC 被 RFC 2401–2412 否决。然后在 2005 年,这些 RFC 被 RFC 4301–4309 更新。

IPsec 子系统非常复杂,可能是 Linux 内核网络堆栈中最复杂的部分。考虑到组织和公民个人日益增长的安全需求,它的重要性是至关重要的。本章为你深入研究这个复杂的子系统提供了一个基础。

一般

IPsec 已经成为国际上大多数 IP 虚拟专用网技术 的标准。也就是说,也有基于不同技术的 VPN,如安全套接字层(SSL)和pptp(通过 GRE 协议隧道化 PPP 连接)。在 IPsec 的几种操作模式中,最重要的是传输模式和隧道模式。在传输模式下,只有 IP 数据包的有效载荷被加密,而在隧道模式下,整个 IP 数据包被加密并插入到具有新 IP 报头的新 IP 数据包中。当使用带有 IPsec 的 VPN 时,您通常在隧道模式下工作,尽管有时您在传输模式下工作(例如,L2TP/IPsec)。

我首先简单讨论一下 IPsec 中的互联网密钥交换(IKE) 用户空间守护进程和加密技术。这些主题大多不是内核网络堆栈的一部分,但是与 IPsec 操作相关,并且需要更好地理解内核 IPsec 子系统。接下来我将讨论 XFRM 框架,它是 IPsec 用户空间部分和 IPsec 内核组件之间的配置和监控接口,并解释 IPsec 数据包在 Tx 和 Rx 路径中的遍历。我用一小段关于 IPsec 中 NAT 穿越的内容来结束这一章,这是一个重要而有趣的特性,也是一个“快速参考”部分。下一节从 IKE 协议开始讨论。

互联网密钥交换

最流行的开源用户空间 Linux IPsec 解决方案是 Openswan(和从 Openswan 派生出来的libreswan)、strongSwan 和 racoon(属于ipsec-tools)。Racoon 是 Kame 项目的一部分,该项目旨在为 BSD 的变体提供免费的 IPv6 和 IPsec 协议栈实现。

要建立 IPsec 连接,您需要设置安全关联(SA)。 你可以在已经提到的用户空间项目的帮助下完成。SA 由两个参数定义:源地址和 32 位安全参数索引(SPI)。双方 ??(IPsec 术语中称为发起方响应方)要就一个密钥(或多个密钥)、认证、加密、数据完整性和密钥交换算法等参数,以及密钥生存期(仅 IKEv1)等其他参数达成一致。这可以通过两种不同的密钥分发方式来实现:手动密钥交换(由于安全性较低,很少使用)或 IKE 协议。Openswan 和 strongSwan 实现提供了一个 IKE 守护进程(Openswan 中的pluto和 strongSwan 中的charon),它使用 UDP 端口 500(源和目的地)来发送和接收 IKE 消息。两者都使用 XFRM Netlink 接口与 Linux 内核的本地 IPsec 栈通信。strongSwan 项目是 RFC 5996“互联网密钥交换协议第 2 版(IKEv2)”唯一完整的开源实现,而 Openswan 项目仅实现了一小部分强制子集。

可以在 Openswan 和 strongSwan 5.x 中使用 IKEv1 Aggressive 模式(对于 strongSwan,应该显式配置,在这种情况下charon守护进程的名称改为weakSwan);但是这种选择被认为是不安全的。由于内置了racoon的遗留客户端,IKEv1 仍被苹果操作系统(iOS 和 Mac OS X)使用。虽然许多实现使用 IKEv1,但是使用 IKEv2 有许多改进和优点。我将非常简要地提到其中的一些:在 IKEv1 中,建立一个 SA 比在 IKEv2 中需要更多的消息。IKEv1 非常复杂,而 IKEv2 要简单得多,也更健壮,主要是因为每个 IKEv2 请求消息都必须得到 IKEv2 响应消息的确认。在 IKEv1 中,没有确认,但有一个退避算法,在数据包丢失的情况下,它会一直尝试下去。然而,在 IKEv1 中,当双方执行重新传输时,可能会出现竞争,而在 IKEv2 中,这种情况不会发生,因为重新传输的责任只在发起方。IKEv2 的其他重要功能包括:IKEv2 集成了 NAT 穿越支持、流量选择器的自动缩小(left|rightsubnet两端不必完全匹配,但一个建议可以是另一个建议的子集)、允许分配虚拟 IPv4/IPv6 地址和内部 DNS 信息的 IKEv2 配置有效负载(替换 IKEv1 模式配置),以及 IKEv2 EAP 认证(替换危险的 IKEv1 扩展验证协议), 它通过在客户端使用潜在的弱 EAP 验证算法(例如 EAP-MSCHAPv2)之前首先请求 VPN 服务器证书和数字签名来解决潜在的弱 PSK 问题。

IKE 分为两个阶段:第一阶段称为主模式。 在这个阶段,每一方验证另一方的身份,使用 Diffie-Hellman 密钥交换算法建立一个共同的会话密钥。这种相互认证是基于 RSA 或 ECDSA 证书或预共享秘密(预共享密钥,PSKs),它们是基于密码的,被认为是较弱的。其他参数,如加密算法和要使用的身份验证方法也需要协商。如果这个阶段成功完成,这两个对等体就建立了 ISAKMP SA(互联网安全协会密钥管理协议安全协会)。第二阶段称为快速模式。在这个阶段, 双方就使用的密码算法达成一致。IKEv2 协议不区分阶段 1 和阶段 2,而是建立第一个 CHILD_SA 作为 IKE_AUTH 消息交换的一部分。CHILD_SA_CREATE 消息交换仅用于建立附加的 CHILD_SA,或者用于 IKE 和 IPsec SAs 的定期密钥更新。这就是为什么 IKEv1 需要九条消息来建立单个 IPsec SA,而 IKEv2 只需要四条消息就可以做到这一点。

下一节将简要讨论 IPsec 环境中的加密技术(对该主题更全面的讨论超出了本书的范围)。

IPsec 和密码学

Linux 有两个广泛使用的 IPsec 栈:在 2.6 内核中引入的本地 Netkey 栈(由 Alexey Kuznetsov 和 David S. Miller 开发),以及最初为 2.0 内核编写的 KLIPS 栈(它早于 netfilter!).Netkey 使用 Linux 内核加密 API,而 KLIPS 可能通过开放加密框架(OCF) 支持更多的加密硬件。OCF 的优势在于它支持使用异步调用来加密/解密数据。在 Linux 内核中,大多数加密 API 执行同步调用。我应该提一下acrypto内核代码,它是 Linux 内核的异步加密层。所有算法类型都有异步实现。许多硬件加密加速器使用异步加密接口来卸载加密请求。这仅仅是因为在加密工作完成之前,他们不能阻塞。他们必须使用异步 API。

异步 API 也可以使用软件实现的算法。例如,cryptd crypto 模板可以在异步模式下运行任意算法。并且在多核环境下工作时可以使用pcrypt crypto 模板。该模板通过向一组可配置的 CPU 发送传入的加密请求来并行化加密层。它还负责加密请求的顺序,因此在与 IPsec 一起使用时不会引入数据包重新排序。在某些情况下,pcrypt的使用可以大幅提高 IPsec 的速度。加密层有一个用户管理 API,被crconf ( http://sourceforge.net/projects/crconf/)工具用来配置加密层,因此异步加密算法可以在任何需要的时候配置。随着 2008 年发布的 Linux 2.6.25 内核,XFRM 框架开始为非常高效的 AEAD(关联数据认证加密)算法(例如 AES-GCM)提供支持,尤其是在英特尔 AES-NI 指令集可用且数据完整性几乎免费的情况下。深入研究 IPsec 中的加密技术超出了本书的范围。要了解更多信息,我建议阅读威廉·斯塔林斯(Prentice Hall,2013)的网络安全基础知识第五版中的相关章节。

下一节讨论 XFRM 框架,它是 IPsec 的基础设施。

XFRM 框架

IPsec 是由 XFRM(发音为“transform”)框架实现的,该框架起源于 USAGI 项目,旨在提供生产质量的 IPv6 和 IPsec 协议栈。术语转换指的是根据某种 IPsec 规则在内核堆栈中转换的传入数据包或传出数据包。XFRM 框架是在内核 2.5 中引入的。XFRM 基础设施是独立于协议族的,这意味着 IPv4 和 IPv6 有一个通用部分,位于net/xfrm下。IPv4 和 IPv6 都有自己的 ESP、AH 和 IPCOMP 实现。比如 IPv4 ESP 模块是net/ipv4/esp4.c,IPv6 ESP 模块是net/ipv6/esp6.c。除此之外,IPv4 和 IPv6 实现了一些特定于协议的模块来支持 XFRM 基础设施,比如net/ipv4/xfrm4_policy.cnet/ipv6/xfrm6_policy.c

XFRM 框架支持网络名称空间,这是一种轻量级进程虚拟化的形式,使单个进程或一组进程拥有自己的网络堆栈(我在第十四章中讨论了网络名称空间)。每个网络名称空间(struct net的实例)包括一个名为xfrm的成员,它是netns_xfrm结构的一个实例。这个对象包含了很多你在本章会遇到的数据结构和变量,比如 XFRM 策略的哈希表和 XFRM 状态的哈希表、sysctl参数、XFRM 状态垃圾收集器、计数器等等:

struct netns_xfrm {
        struct hlist_head       *state_bydst;
        struct hlist_head       *state_bysrc;
        struct hlist_head       *state_byspi;
        . . .
        unsigned int            state_num;
        . . .

        struct work_struct      state_gc_work;

     . . .

        u32                     sysctl_aevent_etime;
        u32                     sysctl_aevent_rseqth;
        int                     sysctl_larval_drop;
        u32                     sysctl_acq_expires;
};
(include/net/netns/xfrm.h)

XFRM 初始化

在 IPv4 中,XFRM 初始化是通过从net/ipv4/route.c中的ip_rt_init()方法调用xfrm_init()方法和xfrm4_init()方法来完成的。在 IPv6 中,从ip6_route_init()方法调用xfrm6_init()方法来执行 XFRM 初始化。用户空间和内核之间的通信是通过创建 NETLINK_XFRM netlink 套接字以及发送和接收 NETLINK 消息来完成的。netlink NETLINK_XFRM 内核套接字的创建方法如下:

static int __net_init xfrm_user_net_init(struct net *net)
{
        struct sock *nlsk;
        struct netlink_kernel_cfg cfg = {
                .groups = XFRMNLGRP_MAX,
                .input  = xfrm_netlink_rcv,
        };

        nlsk = netlink_kernel_create(net, NETLINK_XFRM, &cfg);
        . . .
        return 0;
}

从用户空间发送的消息(像 XFRM_MSG_NEWPOLICY 用于创建新的安全策略或 XFRM_MSG_NEWSA 用于创建新的安全关联)由xfrm_netlink_rcv()方法(net/xfrm/xfrm_user.c)处理,该方法又调用xfrm_user_rcv_msg()方法(我在第二章中讨论 netlink 套接字)。

XFRM 策略和 XFRM 状态是 XFRM 框架的基本数据结构。我首先描述什么是 XFRM 策略,然后描述什么是 XFRM 状态。

XFRM 策略

安全策略是告诉 IPsec 某个流是否应该被处理或者是否可以绕过 IPsec 处理的规则。xfrm_policy结构代表一个 IPsec 策略。一个策略包括一个选择器(一个xfrm_selector对象)。当策略的选择器与流匹配时,将应用策略。XFRM 选择器由源地址和目的地址、源端口和目的端口、协议等字段组成,这些字段可以标识流:

struct xfrm_selector {
        xfrm_address_t  daddr;
        xfrm_address_t  saddr;
        __be16  dport;
        __be16  dport_mask;
        __be16  sport;
        __be16  sport_mask;
        __u16   family;
        __u8    prefixlen_d;
        __u8    prefixlen_s;
        __u8    proto;
        int     ifindex;
        __kernel_uid32_t        user;
};
(include/uapi/linux/xfrm.h)

xfrm_selector_match()方法获取 XFRM 选择器、流和族(IPv4 的 AF_INET 或 IPv6 的 AF_INET6)作为参数,当指定的流与指定的 XFRM 选择器匹配时,返回true。请注意,xfrm_selector结构也用于 XFRM 状态,您将在本节的后面看到。安全策略由xfrm_policy结构表示:

struct xfrm_policy {
        . . .
        struct hlist_node             bydst;
        struct hlist_node             byidx;

        /* This lock only affects elements except for entry. */
        rwlock_t                      lock;
        atomic_t                      refcnt;
        struct timer_list             timer;

        struct flow_cache_object      flo;
        atomic_t                      genid;
        u32                           priority;
        u32                           index;
        struct xfrm_mark              mark;
        struct xfrm_selector          selector;
        struct xfrm_lifetime_cfg      lft;
        struct xfrm_lifetime_cur      curlft;
        struct xfrm_policy_walk_entry walk;
        struct xfrm_policy_queue      polq;
        u8                            type;
        u8                            action;
        u8                            flags;
        u8                            xfrm_nr;
        u16                           family;
        struct xfrm_sec_ctx           *security;
        struct xfrm_tmpl              xfrm_vec[XFRM_MAX_DEPTH];
};
(include/net/xfrm.h)

以下描述涵盖了xfrm_policy结构的重要成员:

  • refcnt:XFRM 策略引用计数器;在xfrm_policy_alloc( )方法中初始化为 1,由xfrm_pol_hold()方法递增,由xfrm_pol_put()方法递减。
  • timer:每策略定时器;在xfrm_policy_alloc()方法中,定时器回调被设置为xfrm_policy_timer()xfrm_policy_timer()方法处理策略过期:它负责通过调用xfrm_policy_delete()方法删除过期的策略,并通过调用km_policy_expired()方法向所有注册的密钥管理器发送事件(XFRM_MSG_POLEXPIRE)。
  • lft:XFRM 策略生存期(xfrm_lifetime_cfg对象)。每个 XFRM 策略都有一个生存期,它是一个时间间隔(用时间或字节数表示)。

您可以使用ip命令和limit参数设置 XFRM 策略生存期值,例如:

ip xfrm policy add src 172.16.2.0/24 dst 172.16.1.0/24 limit byte-soft 6000 ...
  • 将 XFRM 策略生存期(lft)的soft_byte_limit设置为 6000;参见man 8 ip xfrm

通过在运行ip -stat xfrm policy show时检查生命周期配置条目,可以显示 XFRM 策略的生命周期(lft)。

  • curlft:XFRM 策略当前生命周期,,反映了生命周期上下文中策略的当前状态。curlft是一个xfrm_lifetime_cur对象。它由四个成员组成(它们都是 64 位的字段,无符号):

  • bytes:由 IPsec 子系统处理的字节数,在 Tx 路径中通过xfrm_output_one()方法递增,在 Rx 路径中通过xfrm_input()方法递增。

  • packets:IPsec 子系统处理的数据包数量,在 Tx 路径中通过xfrm_output_one()方法递增,在 Rx 路径中通过xfrm_input()方法递增。

  • add_time:添加策略的时间戳,在添加策略时初始化,在xfrm_policy_insert()方法和xfrm_sk_policy_insert()方法中。

  • use_time: The timestamp of last access to the policy. The use_time timestamp is updated, for example, in the xfrm_lookup() method or in the __xfrm_policy_check() method. Initialized to 0 when adding the XFRM policy, in the xfrm_policy_insert() method and in the xfrm_sk_policy_insert() method.

    image 注意您可以在运行ip -stat xfrm policy show时通过检查 lifetime current 条目来显示 XFRM 策略的当前 lifetime ( curlft)对象。

  • polq:一个队列,用于保存在没有 XFRM 状态与策略相关联时发送的数据包。默认情况下,这样的包通过调用make_blackhole()方法被丢弃。当xfrm_larval_drop sysctl条目设置为 0 ( /proc/sys/net/core/xfrm_larval_drop时,这些数据包被保存在 SKBs 的一个队列(polq.hold_queue)中;这个队列中最多可以保存 100 个数据包(XFRM_MAX_QUEUE_LEN)。这是通过用xfrm_create_dummy_bundle()方法创建一个虚拟 XFRM 包来实现的(详见本章后面的“XFRM 查找”一节)。默认情况下,xfrm_larval_drop sysctl条目被设置为 1(参见net/xfrm/xfrm_sysctl.c中的__xfrm_sysctl_init()方法)。

  • type:通常类型为 XFRM_POLICY_TYPE_MAIN (0)。当内核支持子策略(CONFIG_XFRM_SUB_POLICY 已设置)时,两个策略可以应用于同一个数据包,您可以使用 XFRM_POLICY_TYPE_SUB (1)类型。在内核中存在时间较短的策略应该是子策略。通常只有开发人员/调试和移动 IPv6 才需要此功能,因为您可能对 IPsec 应用一个策略,对移动 IPv6 应用一个策略。IPsec 策略通常是主策略,其生命周期比移动 IPv6(子)策略长。

  • action : 可以有以下两个值之一:

  • XFRM_POLICY_ALLOW (0):允许流量。

  • XFRM_POLICY_BLOCK(1):禁止流量(例如,在/etc/ipsec.conf中使用type=rejecttype=drop时)。

  • xfrm_nr:与策略相关联的模板数量—最多可以有六个模板(XFRM_MAX_DEPTH)。xfrm_tmpl结构是 XFRM 状态和 XFRM 策略之间的中间结构。它在copy_templates()方法net/xfrm/xfrm_user.c中被初始化。

  • family : IPv4 或 IPv6。

  • security:安全上下文(xfrm_sec_ctx对象),允许 XFRM 子系统限制可以通过安全关联(XFRM 状态)发送或接收数据包的套接字。更多细节请见http://lwn.net/Articles/156604/

  • xfrm_vec:XFRM 模板的数组(xfrm_tmpl对象)。

内核将 IPsec 安全策略存储在安全策略数据库(SPD) 中。SPD 的管理是通过从用户空间套接字发送消息来完成的。比如:

  • 添加 XFRM 策略(XFRM_MSG_NEWPOLICY)由xfrm_add_policy()方法处理。
  • 删除 XFRM 策略(XFRM_MSG_DELPOLICY)由xfrm_get_policy()方法处理。
  • 显示 SPD (XFRM_MSG_GETPOLICY)由xfrm_dump_policy()方法处理。
  • 刷新 SPD (XFRM_MSG_FLUSHPOLICY)是由xfrm_flush_policy()方法处理的。

下一节描述什么是 XFRM 状态。

XFRM 状态(安全关联)

xfrm_state结构表示一个 IPsec 安全关联(SA) ( include/net/xfrm.h)。它代表单向流量,包括加密密钥、标志、请求 id、统计信息、重放参数等信息。通过从用户空间套接字发送请求(XFRM_MSG_NEWSA)来添加 XFRM 状态;它在内核中由xfrm_state_add()方法(net/xfrm/xfrm_user.c)处理。同样,通过发送 XFRM_MSG_DELSA 消息来删除状态,它在内核中由xfrm_del_sa()方法处理:

struct xfrm_state {
        . . .
        union {
                struct hlist_node       gclist;
                struct hlist_node       bydst;
        };
        struct hlist_node       bysrc;
        struct hlist_node       byspi;

        atomic_t                refcnt;
        spinlock_t              lock;

        struct xfrm_id          id;
        struct xfrm_selector    sel;
        struct xfrm_mark        mark;
        u32                     tfcpad;

        u32                     genid;

        /* Key manager bits */
        struct xfrm_state_walk  km;

        /* Parameters of this state. */
        struct {
                u32             reqid;
                u8              mode;
                u8              replay_window;
                u8              aalgo, ealgo, calgo;
                u8              flags;
                u16             family;
                xfrm_address_t  saddr;
                int             header_len;
                int             trailer_len;
        } props;

        struct xfrm_lifetime_cfg lft;

        /* Data for transformer */
        struct xfrm_algo_auth   *aalg;
        struct xfrm_algo        *ealg;
        struct xfrm_algo        *calg;
        struct xfrm_algo_aead   *aead;

        /* Data for encapsulator */
        struct xfrm_encap_tmpl  *encap;

        /* Data for care-of address */
        xfrm_address_t  *coaddr;

        /* IPComp needs an IPIP tunnel for handling uncompressed packets */
        struct xfrm_state       *tunnel;

        /* If a tunnel, number of users + 1 */
        atomic_t                tunnel_users;

        /* State for replay detection */
        struct xfrm_replay_state replay;
        struct xfrm_replay_state_esn *replay_esn;

        /* Replay detection state at the time we sent the last notification */
        struct xfrm_replay_state preplay;
        struct xfrm_replay_state_esn *preplay_esn;

        /* The functions for replay detection. */
        struct xfrm_replay      *reply;

        /* internal flag that only holds state for delayed aevent at the
         * moment
        */
        u32                     xflags;

        /* Replay detection notification settings */
        u32                     replay_maxage;
        u32                     replay_maxdiff;

        /* Replay detection notification timer */
        struct timer_list       rtimer;

        /* Statistics */
        struct xfrm_stats       stats;

        struct xfrm_lifetime_cur curlft;
        struct tasklet_hrtimer  mtimer;

        /* used to fix curlft->add_time when changing date */
        long            saved_tmo;

        /* Last used time */
        unsigned long           lastused;

        /* Reference to data common to all the instances of this
         * transformer. */
        const struct xfrm_type  *type;
        struct xfrm_mode        *inner_mode;
        struct xfrm_mode        *inner_mode_iaf;
        struct xfrm_mode        *outer_mode;

        /* Security context */
        struct xfrm_sec_ctx     *security;

        /* Private data of this transformer, format is opaque,
         * interpreted by xfrm_type methods. */
        void                    *data;
};
(include/net/xfrm.h)

以下描述详细介绍了xfrm_state结构的一些重要成员:

  • refcnt:参考计数器,由xfrm_state_hold()方法递增,由__xfrm_state_put()方法或xfrm_state_put()方法递减(当参考计数器达到 0 时,后者也通过调用__xfrm_state_destroy()方法释放 XFRM 状态)。

  • id:id(xfrm_id对象)由三个唯一定义它的字段组成:目的地址、spi 和安全协议(AH、ESP 或 IPCOMP)。

  • props:XFRM 状态的属性。例如:

  • mode:可以是五种模式之一(例如,传输模式为 XFRM_MODE_TRANSPORT,隧道模式为 XFRM _ MODE _ TUNNEL 参见include/uapi/linux/xfrm.h

  • flag:比如 XFRM_STATE_ICMP。这些标志在include/uapi/linux/xfrm.h中可用。这些标志可以从用户空间设置,例如,使用ip命令和flag选项:ip xfrm add state flag icmp

  • family:IPv6 的 IPv4。

  • saddr:XFRM 状态的源地址。

  • lft:XFRM 状态生存期(xfrm_lifetime_cfg对象)。

  • stats:一个xfrm_stats对象,代表 XFRM 状态统计。您可以通过ip –stat xfrm show显示 XFRM 状态统计。

内核将 IPsec 安全关联存储在安全关联数据库(SAD)中。xfrm_state对象存储在netns_xfrm(前面讨论过的 XFRM 名称空间)的三个散列表中:state_bydststate_bysrcstate_byspi。这些表的关键字分别由xfrm_dst_hash()xfrm_src_hash()xfrm_spi_hash()方法计算。当添加一个xfrm_state对象时,它被插入到这三个散列表中。如果 spi 的值为 0(值 0 通常不用于 SPI——当它为 0 时,我将很快提到),则xfrm_state对象不会添加到state_byspi哈希表中(参见net/xfrm/xfrm_state.c中的__xfrm_state_insert()方法)。

image 注意值为 0 的 spi 仅用于采集状态。内核向密钥管理器发送获取消息,如果流量与策略匹配,则添加一个带有 spi 0 的临时获取状态,但该状态尚未解决。只要获取状态存在,内核就不会费心发送进一步的获取;可以在net->xfrm.sysctl_acq_expires处配置寿命。如果状态得到解决,这个获取状态将被实际状态替换。

可以通过以下方式在 SAD 中进行查找:

  • xfrm_state_lookup()方法:在state_byspi哈希表中。
  • xfrm_state_lookup_byaddr()方法:在state_bysrc哈希表中。
  • xfrm_state_find()方法:在state_bydst哈希表中。

ESP 协议是最常用的 IPsec 协议;它支持加密和认证。下一节讨论 IPv4 ESP 实现。

ESP 实施(IPv4)

RFC 4303 中规定了电潜泵协议;它支持加密和认证。虽然它也支持仅加密和仅身份验证模式,但它通常与加密和身份验证一起使用,因为这样更安全。这里我还应该提到 AES-GCM 等新的认证加密(AEAD)方法,它可以在一个通道中完成加密和数据完整性计算,并且可以在多个内核上高度并行化,因此借助英特尔 AES-NI 指令集,可以实现几个 Gbit/s 的 IPsec 吞吐量。ESP 协议支持隧道模式和传输模式;协议标识符是 50 (IPPROTO_ESP)。ESP 为每个数据包添加新的报头和报尾。根据图 10-1 所示的 ESP 格式,有以下字段:

  • SPI: 一个 32 位的安全参数索引。 和源地址一起标识一个 SA。
  • 序列号: 32 位,每发送一个包递增 1,以防止重放攻击。
  • 净荷数据: 一个可变大小的加密数据块。
  • *填充:*为加密数据块填充 以满足对齐要求(0-255 字节)。
  • *填充长度:*以字节为单位的填充大小(1 字节)。
  • *下一个报头:*下一个报头的类型(1 字节)。
  • 认证数据: 【完整性校验值】(ICV)。

9781430261964_Fig10-01.jpg

图 10-1 。ESP 格式

下一节讨论 IPv4 ESP 初始化。

IPv4 ESP 初始化

我们首先定义一个esp_type ( xfrm_type对象)和esp4_protocol ( net_protocol对象),然后注册它们:

static const struct xfrm_type esp_type =
{
        .description    = "ESP4",
        .owner          = THIS_MODULE,
        .proto          = IPPROTO_ESP,
        .flags          = XFRM_TYPE_REPLAY_PROT,
        .init_state     = esp_init_state,
        .destructor     = esp_destroy,
        .get_mtu        = esp4_get_mtu,
        .input          = esp_input,
        .output         = esp_output
};

static const struct net_protocol esp4_protocol = {
        .handler        =       xfrm4_rcv,
        .err_handler    =       esp4_err,
        .no_policy      =       1,
        .netns_ok       =       1,
};

static int __init esp4_init(void)
{

每个协议族都有一个xfrm_state_afinfo对象的实例,它包括协议族特定的状态方法;因此,IPv4 有xfrm4_state_afinfo(net/ipv4/xfrm4_state.c),IPv6 有xfrm6_state_afinfo。这个对象包括一个名为type_mapxfrm_type对象数组。通过调用xfrm_register_type()方法注册 XFRM 类型会将指定的xfrm_type设置为该数组中的一个元素:

        if (xfrm_register_type(&esp_type, AF_INET) < 0) {
                pr_info("%s: can't add xfrm type\n", __func__);
                return -EAGAIN;
        }

注册 IPv4 ESP 协议就像注册任何其他 IPv4 协议一样,通过调用inet_add_protocol()方法来完成。注意,IPv4 ESP 使用的协议处理程序,即xfrm4_rcv()方法,也被 IPv4 AH 协议(net/ipv4/ah4.c)和 IPv4 IPCOMP (IP 有效载荷压缩协议)协议(net/ipv4/ipcomp.c)使用。

        if (inet_add_protocol(&esp4_protocol, IPPROTO_ESP) < 0) {
                pr_info("%s: can't add protocol\n", __func__);
                xfrm_unregister_type(&esp_type, AF_INET);
                return -EAGAIN;
        }
        return 0;
}
(net/ipv4/esp4.c)

接收 IPsec 数据包(传输模式)

假设您在 IPv4 的传输模式下工作,您收到了一个目的地为本地主机的 ESP 数据包。传输模式下的 ESP 不加密 IP 报头,只加密 IP 有效负载。图 10-2 显示了一个传入的 IPv4 ESP 数据包的遍历,本节描述了其各个阶段。我们将通过本地交付的所有通常阶段,从ip_rcv()方法开始,我们将到达ip_local_deliver_finish()方法。因为 IPv4 报头中的 protocol 字段的值是 ESP (50),所以我们调用它的处理程序,这就是xfrm4_rcv()方法,正如您前面看到的。xfrm4_rcv()方法进一步调用通用的xfrm_input()方法,后者通过调用xfrm_state_lookup()方法在 SAD 中执行查找。如果查找失败,数据包将被丢弃。在查找命中的情况下,调用相应 IPsec 协议的input回调方法:

int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type)
{
         struct xfrm_state *x;
         do {
                 . . .

9781430261964_Fig10-02.jpg

图 10-2 。正在接收 IPv4 ESP 数据包,本地传递,传输模式。注意:图中描述了一个 IPv4 ESP 数据包。对于 IPv4 AH 包,调用 ah_input()方法,而不是 esp_input()方法;同样,对于 IPv4 IPCOMP 数据包,将调用 ipcomp_input()方法,而不是 esp_input()方法

state_byspi散列表中执行查找:

x = xfrm_state_lookup(net, skb->mark, daddr, spi, nexthdr, family);

如果查找失败,则静默丢弃数据包:

if (x == NULL) {
                         XFRM_INC_STATS(net, LINUX_MIB_XFRMINNOSTATES);
                         xfrm_audit_state_notfound(skb, family, spi, seq);
                         goto drop;
}

在这种情况下,对于 IPv4 ESP 传入流量,与状态(x->type)相关联的 XFRM 类型是 ESP XFRM 类型(esp_type);它的input回调被设置为esp_input(),如前面的“IPv4 ESP 初始化”一节所述。

通过调用x->type->input(),在下面一行中esp_input()方法被调用;此方法返回原始数据包在被 ESP 加密之前的协议号:

nexthdr = x->type->input(x, skb);
. . .

使用 XFRM_MODE_SKB_CB 宏将原始协议号保存在 SKB 的控制缓冲区(cb)中;稍后将使用它来修改数据包的 IPv4 报头,您将会看到:

XFRM_MODE_SKB_CB(skb)->protocol = nexthdr;

esp_input()方法终止后,调用xfrm4_transport_finish()方法。该方法修改 IPv4 报头的各个字段。看一看xfrm4_transport_finish()的方法:

int xfrm4_transport_finish(struct sk_buff *skb, int async)
{
        struct iphdr *iph = ip_hdr(skb);

IPv4 头(iph->protocol)的协议此时为 50(ESP);您应该将它设置为原始数据包的协议号(在它被 ESP 加密之前),以便它将被 L4 套接字处理。原始数据包的协议号保存在XFRM_MODE_SKB_CB(skb)->protocol中,正如您在本节前面看到的:

iph->protocol = XFRM_MODE_SKB_CB(skb)->protocol;

. . .
__skb_push(skb, skb->data - skb_network_header(skb));
iph->tot_len = htons(skb->len);

自 IPv4 标头被修改后,重新计算校验和:

ip_send_check(iph);

调用任何 netfilter NF_INET_PRE_ROUTING 钩子回调,然后调用xfrm4_rcv_encap_finish()方法:

        NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, skb->dev, NULL,
                xfrm4_rcv_encap_finish);
        return 0;
}

xfrm4_rcv_encap_finish()方法调用ip_local_deliver()方法。现在,IPv4 报头中的protocol成员的值是原始的传输协议(UDPv4、TCPv4 等等),所以从现在开始,您继续进行通常的数据包遍历,数据包被传递到传输层(L4)。

发送 IPsec 数据包(传输模式)

图 10-3 显示了在传输模式下通过 IPv4 ESP 发送的输出数据包的 Tx 路径。在路由子系统中执行查找(通过调用ip_route_output_flow()方法)后的第一步是执行 XFRM 策略的查找,该策略可以应用于这个流。您可以通过调用xfrm_lookup()方法来实现这一点(我将在本节稍后讨论该方法的内部机制)。如果有查找命中,继续到ip_local_out()方法,然后,在调用如图图 10-3 所示的几个方法后,最终到达esp_output()方法,该方法加密数据包,然后通过调用ip_output()方法发送出去。

9781430261964_Fig10-03.jpg

图 10-3 。传输 IPv4 ESP 数据包,传输模式。为了简单起见,省略了创建虚拟束的情况(当没有 XFRM 状态时)和一些其他细节

下一节讨论如何在 XFRM 中执行查找。

XFRM 查找

系统发出的每个数据包都会调用xfrm_lookup()方法 。您希望这种查找尽可能高效。为了实现这个目标,使用了包。Bundles 允许您缓存重要的信息,比如路由、策略、策略数量等等;这些包是xfrm_dst结构的实例,通过使用流缓存来存储。当某个流的第一个包到达时,您在通用流缓存中创建一个条目,然后创建一个包(xfrm_dst对象)。在对该包的查找失败之后,完成包的创建,因为它是该流的第一个包。当此流的后续数据包到达时,您将会在执行流缓存查找时得到一个命中结果:

struct xfrm_dst {
        union {
                struct dst_entry        dst;
                struct rtable           rt;
                struct rt6_info         rt6;
        } u;
        struct dst_entry *route;
        struct flow_cache_object flo;
        struct xfrm_policy *pols[XFRM_POLICY_TYPE_MAX];
        int num_pols, num_xfrms;
#ifdef CONFIG_XFRM_SUB_POLICY
        struct flowi *origin;
        struct xfrm_selector *partner;
#endif
        u32 xfrm_genid;
        u32 policy_genid;
        u32 route_mtu_cached;
        u32 child_mtu_cached;
        u32 route_cookie;
        u32 path_cookie;
};
(include/net/xfrm.h)

xfrm_lookup()方法是一个非常复杂的方法。我讨论了它的重要部分,但我没有深入研究它的所有细微差别。图 10-4 显示了xfrm_lookup()方法的内部框图。

9781430261964_Fig10-04.jpg

图 10-4 。xfrm_lookup()内部

我们来看看xfrm_lookup()方法:

struct dst_entry *xfrm_lookup(struct net *net, struct dst_entry *dst_orig,
                              const struct flowi *fl, struct sock *sk, int flags)
{

xfrm_lookup()方法只处理 Tx 路径;因此,您通过以下方式将流向(dir)设置为 FLOW_DIR_OUT:

         u8 dir = policy_to_flow_dir(XFRM_POLICY_OUT);

如果一个策略与这个套接字相关联,您可以通过xfrm_sk_policy_lookup()方法执行查找,检查数据包流是否与策略选择器匹配。请注意,如果要转发数据包,则从__xfrm_route_forward()方法、 中调用了xfrm_lookup()方法,并且没有与数据包相关联的套接字,因为它不是在本地主机上生成的;在这种情况下,指定的sk参数为空:

        if (sk && sk->sk_policy[XFRM_POLICY_OUT]) {
                num_pols = 1;
                pols[0] = xfrm_sk_policy_lookup(sk, XFRM_POLICY_OUT, fl);

        . . .
}

如果没有与这个套接字相关联的策略,那么通过调用flow_cache_lookup()方法,将指向xfrm_bundle_lookup方法的函数指针作为参数传递(resolver回调),在通用流缓存中执行查找。查找的关键是流对象(指定的fl参数)。如果在流缓存中没有找到条目,请分配一个新的流缓存条目。如果找到一个具有相同genid的条目,通过调用flo->ops->get(flo)来调用xfrm_bundle_flo_get()方法。最后,您通过调用resolver回调来调用xfrm_bundle_lookup()方法,该回调获取流对象作为参数(oldflo)。参见net/core/flow.cflow_cache_lookup()方法 的实现:

flo = flow_cache_lookup(net, fl, family, dir, xfrm_bundle_lookup, dst_orig);

获取包含流缓存对象作为成员的包(xfrm_dst对象):

xdst = container_of(flo, struct xfrm_dst, flo);

获取缓存数据,如策略数量、模板数量、策略和路线:

       num_pols = xdst->num_pols;
       num_xfrms = xdst->num_xfrms;
       memcpy(pols, xdst->pols, sizeof(struct xfrm_policy*) * num_pols);
       route = xdst->route;
}

dst = &xdst->u.dst;

接下来是处理虚拟包。一个虚拟束是 一个其中路由成员为空的束。当没有找到 XFRM 状态时,通过调用xfrm_create_dummy_bundle()方法在 XFRM 包查找过程中创建它(通过xfrm_bundle_lookup()方法)。在这种情况下,根据sysctl_larval_drop ( /proc/sys/net/core/xfrm_larval_drop)的值,可以选择两个选项中的一个:

  • 如果sysctl_larval_drop被置位(这意味着它的值是 1——默认情况下是这样,如本章前面提到的),该数据包应该被丢弃。

  • 如果未设置sysctl_larval_drop(其值为 0),则数据包保存在每个策略的队列(polq.hold_queue)中,该队列最多可包含 100 个(XFRM _ MAX _ QUEUE _ LEN)skb;这是通过xdst_queue_output()方法实现的。这些数据包会一直保留,直到 XFRM 状态被解析或超时。一旦状态被解析,包就被送出队列。如果在一段时间间隔后 XFRM 状态没有得到解决(xfrm_policy_queue对象的超时),那么通过xfrm_queue_purge()方法:

    if (route == NULL && num_xfrms > 0) {
            /* The only case when xfrm_bundle_lookup() returns a
             * bundle with null route, is when the template could
             * not be resolved. It means policies are there, but
             * bundle could not be created, since we don't yet
             * have the xfrm_state's. We need to wait for KM to
             * negotiate new SA's or bail out with error.*/
             if (net->xfrm.sysctl_larval_drop) {
    

    刷新队列

对于 IPv4,make_blackhole()方法调用ipv4_blackhole_route()方法。对于 IPv6,它调用ip6_blackhole_route()方法:

        return make_blackhole(net, family, dst_orig);
}

下一节将介绍 IPsec 最重要的特性之一—NAT 穿越,并解释它是什么以及为什么需要它。

IPsec 中的 NAT 穿越

为什么 NAT 设备不允许 IPsec 流量通过?NAT 会改变 IP 地址,有时还会改变数据包的端口号。因此,它会重新计算 TCP 或 UDP 报头的校验和。传输层校验和计算考虑了 IP 地址的来源和目的地。因此,即使只更改了 IP 地址,也应该重新计算 TCP 或 UDP 校验和。但是,在传输模式下使用 ESP 加密,NAT 设备无法更新校验和,因为 TCP 或 UDP 报头是使用 ESP 加密的。有些协议的校验和不包含 IP 报头(如 SCTP),所以这个问题不会发生。为了解决这些问题,开发了 IPsec 的 NAT 穿越标准(或者,按照 RFC 3948 中的官方术语,“IPsec ESP 数据包的 UDP 封装”)。UDP 封装可以应用于 IPv4 数据包,也可以应用于 IPv6 数据包。NAT 穿越解决方案不限于 IPsec 流量;这些技术通常是客户端到客户端的网络应用所需要的,尤其是点对点和基于互联网协议的语音(VoIP)应用。

VoIP NAT 穿越有一些部分解决方案,比如 STUN,TURN,ICE 等等。我应该在这里提到,strongSwan 实现了 IKEv2 中介扩展服务(http://tools.ietf.org/html/draft-brunner-ikev2-mediation-00),它允许位于 NAT 路由器后面的两个 VPN 端点使用类似于 TURN 和 ice 的机制建立直接的对等 IPsec 隧道。例如,STUN 用于 VoIP 开源 Ekiga 客户端(以前称为 gnomemeeting)。这些解决方案的问题是它们不能处理 NAT 设备。称为 SBCs (会话边界控制器)的设备为 VoIP 中的 NAT 穿越提供了完整的解决方案。SBC 可以在硬件中实现(例如,Juniper Networks 提供了集成路由器的 SBC 解决方案),也可以在软件中实现。这些 SBC 解决方案对实时协议(RTP)发送的媒体流量执行 NAT 穿越,有时也对会话发起协议(SIP)发送的信令流量执行 NAT 穿越。NAT 穿越在 IKEv2 是可选的。Openswan、strongSwan 和 racoon 支持 NAT 穿越,但 Openswan 和 racoon 仅支持 IKEv1 的 NAT-T,而 strongSwan 支持 IKEv1 和 IKEv2 的 NAT 穿越。

NAT-T 操作模式

NAT 穿越是如何工作的?首先,请记住 NAT-T 仅适用于 ESP 流量,而不适用于 AH。另一个限制是 NAT-T 不能用于手动键控,只能用于 IKEv1 和 IKEv2。这是因为 NAT-T 与交换 IKEv1/IKEv2 消息联系在一起。首先,您必须告诉用户空间守护进程(pluto)您想要使用 NAT 遍历特性,因为它在默认情况下是不激活的。在 Openswan 中,您可以通过将nat_traversal=yes添加到/etc/ipsec.conf中的连接参数来实现这一点。不在 NAT 后面的客户端不受添加此项的影响。在 strongSwan 中,IKEv2 charon守护进程始终支持 NAT 穿越,并且该功能不能被停用。在 IKE 的第一阶段(主模式),你检查两个对等体是否都支持 NAT-T,在 IKEv1 中,当一个对等体支持 NAT-T 时,其中一个 ISAKAMP 头成员(厂商 ID)会告知它是否支持 NAT-T,在 IKEv2,NAT-T 是标准的一部分,不必公布。如果满足这一条件,您可以通过发送 NAT-D 有效载荷消息来检查两个 IPsec 对等方之间的路径中是否有一个或多个 NAT 设备。如果也满足这个条件,NAT-T 通过在 IP 报头和 ESP 报头之间插入 UDP 报头来保护原始 IPsec 编码的分组。UDP 报头中的源端口和目的端口都是 4500。此外,NAT-T 每 20 秒发送一次保活消息,以便 NAT 保留其映射。保持活动消息也在 UDP 端口 4500 上发送,并通过其内容和值(一个字节,0xFF)来识别。当此数据包到达 IPsec 对等方时,在通过 NAT 后,内核会剥离 UDP 报头并解密 ESP 有效负载。参见net/ipv4/xfrm4_input.c中的xfrm4_udp_encap_rcv()方法。

摘要

本章介绍了 IPsec 和 XFRM 框架(IPsec 的基础设施),以及 XFRM 策略和状态(XFRM 框架的基本数据结构)。我还讨论了 IKE、ESP4 实现、传输模式下 ESP4 的 Rx/Tx 路径以及 IPsec 中的 NAT 穿越。第十一章处理以下传输层(L4)协议:UDP、TCP、SCTP 和 DCCP 。接下来的“快速参考”部分涵盖了与本章讨论的主题相关的主要方法,按其上下文排序。

快速参考

我用 IPsec 的重要方法的简短列表来结束这一章。本章提到了其中一些。之后,我包含了一个 XFRM SNMP MIB 计数器表。

方法

先说方法。

bool xfrm _ selector _ match(const struct xfrm _ selector * sel,const struct flowi *fl,无符号短族);

当指定的流与指定的 XFRM 选择器匹配时,该方法返回true。调用 IPv4 的__xfrm4_selector_match()方法或 IPv6 的__xfrm6_selector_match()方法。

int xfrm _ policy _ match(const struct xfrm _ policy * pol,const struct flowi *fl,u8 type,u16 family,int dir);

如果指定的策略可以应用于指定的流,则该方法返回 0,否则返回–errno

struct xfrm _ policy * xfrm _ policy _ alloc(struct net * net,GFP _ t GFP);

这个方法分配并初始化一个 XFRM 策略。它将其引用计数器设置为 1,初始化读写锁,将策略名称空间(xp_net)指定为指定的网络名称空间,将其定时器回调设置为xfrm_policy_timer(),将其状态解析分组队列定时器(policy->polq.hold_timer)回调设置为xfrm_policy_queue_process()

void xfrm _ policy _ destroy(struct xfrm _ policy * policy);

此方法删除指定 XFRM 策略对象的计时器,并释放指定 XFRM 策略内存。

void xfrm _ pol _ hold(struct xfrm _ policy * policy);

此方法将指定 XFRM 策略的引用计数增加 1。

静态内联 void xfrm _ pol _ put(struct xfrm _ policy * policy);

此方法将指定 XFRM 策略的引用计数减 1。如果引用计数达到 0,调用xfrm_policy_destroy()方法。

struct xfrm _ state _ af info * xfrm _ state _ get _ af info(unsigned int family);

该方法返回与指定协议族相关的xfrm_state_afinfo对象。

struct dst _ entry * xfrm _ bundle _ create(struct xfrm _ policy * policy,struct xfrm_state **xfrm,int nx,const struct flowi *fl,struct dst _ entry * dst);

这个方法创建一个 XFRM 包。从xfrm_resolve_and_create_bundle()方法调用。

int policy _ to _ flow _ dir(int dir);

此方法根据指定的策略方向返回流向。例如,当指定方向为 XFRM_POLICY_IN 时,返回 FLOW_DIR_IN,依此类推。

静态 struct xfrm _ dst * xfrm _ create _ dummy _ bundle(struct net * net,struct dst_entry *dst,const struct flowi *fl,int num_xfrms,u16 系列);

这个方法创建了一个虚拟包。当找到策略但没有匹配的状态时,从xfrm_bundle_lookup()方法调用。

struct xfrm _ dst * xfrm _ alloc _ dst(struct net * net,int family);

这个方法分配 XFRM bundle 对象。从xfrm_bundle_create()方法和xfrm_create_dummy_bundle()方法调用。

int xfrm_policy_insert(int dir,struct xfrm_policy *policy,int excl);

该方法将 XFRM 策略添加到 SPD 中。从xfrm_add_policy()方法(net/xfrm/xfrm_user.c)或从pfkey_spdadd()方法(net/key/af_key.c)调用。

int xfrm _ policy _ delete(struct xfrm _ policy * pol,int dir);

此方法释放指定 XFRM 策略对象的资源。需要方向参数(dir)来将每个名称空间netns_xfrm对象中的policy_count中的相应 XFRM 策略计数器减 1。

int xfrm _ state _ add(struct xfrm _ state * x);

此方法将指定的 XFRM 状态添加到 SAD。

int xfrm _ state _ delete(struct xfrm _ state * x);

此方法从 SAD 中删除指定的 XFRM 状态。

void _ _ xfrm _ state _ destroy(struct xfrm _ state * x);

该方法通过将 XFRM 状态添加到 XFRM 状态垃圾列表并激活 XFRM 状态垃圾收集器来释放 XFRM 状态的资源。

int xfrm _ state _ walk(struct net * net,struct xfrm_state_walk *walk,int (*func)(struct xfrm_state ,int,void),void * data);

这个方法遍历所有 XFRM 状态(net->xfrm.state_all)并调用指定的func回调。

struct xfrm _ state * xfrm _ state _ alloc(struct net * net);

该方法分配并初始化 XFRM 状态。

void xfrm _ queue _ purge(struct sk _ buff _ head * list);

该方法刷新每个策略的状态解析队列(polq.hold_queue)。

int xfrm _ input(struct sk _ buff * skb,int nexthdr,__be32 spi,int encap _ type);

这个方法是主要的 Rx IPsec 处理程序。

静态 struct dst _ entry * make _ black hole(struct net * net,u16 家族,struct dst _ entry * dst _ orig);

当没有已解析的状态并且设置了sysctl_larval_drop时,从xfrm_lookup()方法调用该方法。对于 IPv4,make_blackhole()方法调用ipv4_blackhole_route()方法;对于 IPv6,它调用ip6_blackhole_route()方法。

int xdst _ queue _ output(struct sk _ buff * skb);

该方法处理将数据包添加到每策略状态解析数据包队列(pq->hold_queue)。这个队列最多可以包含 100 个(XFRM_MAX_QUEUE_LEN)数据包。

struct net * xs _ net(struct xfrm _ state * x);

该方法返回与指定的xfrm_state对象相关联的名称空间对象(xs_net)。

struct net * XP _ net(const struct xfrm _ policy * XP);

该方法返回与指定的xfrm_policy对象相关联的名称空间对象(xp_net)。

int xfrm _ policy _ id 2 dir(u32 index);

此方法根据指定的索引返回策略的方向。

int esp _ input(struct xfrm _ state * x,struct sk _ buff * skb);

此方法是主要的 IPv4 ESP 协议处理程序。

struct IP _ esp _ HDR * IP _ esp _ HDR(const struct sk _ buf * skb);

此方法返回与指定 SKB 关联的 ESP 头。

int verify _ new policy _ info(struct xfrm _ user policy _ info * p);

该方法验证指定的xfrm_userpolicy_info对象包含有效值。(xfrm_userpolicy_info是从用户空间传递过来的对象)。如果是有效对象,则返回 0,否则返回-EINVAL 或-EAFNOSUPPORT。

桌子

表 10-1 列出了 XFRM SNMP MIB 计数器。

表 10-1。 XFRM SNMP MIB 计数器

|

Linux 符号

|

SNMP (procfs)符号

|

计数器可能递增的方法

|
| — | — | — |
| Linux _ MIB _ xfterminerror | XfrmInError | xfrm_input() |
| Linux _ MIB _ xfterminbuffer error | XfrmInBufferError | xfrm_input(),__xfrm_policy_check() |
| Linux _ MIB _ xfterminhderror | XfrmInHdrError | xfrm_input(),__xfrm_policy_check() |
| Linux _ MIB _ xfrmannotations | XfrmInNoStates | xfrm_input() |
| Linux _ MIB _ xfterminstate protoerror | XfrmInStateProtoError | xfrm_input() |
| Linux _ MIB _ xfterminstate mode error | XfrmInStateModeError | xfrm_input() |
| Linux _ MIB _ xfterminstate sequence error | XfrmInStateSeqError | xfrm_input() |
| Linux _ MIB _ xfterminstate expired | XfrmInStateExpired | xfrm_input() |
| LINUX_MIB_XFRMINSTATEMISMATCH | XfrmInStateMismatch | xfrm_input()__xfrm_policy_check() |
| Linux _ MIB _ xfterminstate invalid | XfrmInStateInvalid | xfrm_input() |
| linux _ mib _ xfrmintmplmismatch | XfrmInTmplMismatch | __xfrm_policy_check() |
| linux _ mib _ xfrminnopols | XfrmInNoPols | __xfrm_policy_check() |
| Linux _ MIB _ xfterminpolblock | XfrmInPolBlock | __xfrm_policy_check() |
| Linux _ MIB _ xfterminpolerror | XfrmInPolError | __xfrm_policy_check() |
| Linux _ MIB _ xfrmouteerror | XfrmOutError | xfrm_output_one(),xfrm_output() |
| linux _ mib _ xfrmoutbundlegenerror | XfrmOutBundleGenError | xfrm_resolve_and_create_bundle() |
| Linux _ MIB _ xfrmoutbundleeckerror | XfrmOutBundleCheckError | xfrm_resolve_and_create_bundle() |
| linux _ mib _ xfrmoutnostates | XfrmOutNoStates | xfrm_lookup() |
| linux _ mib _ xfrmoutstateprotoerror | XfrmOutStateProtoError | xfrm_output_one() |
| linux _ mib _ xfrmoutstatemodeerror | XfrmOutStateModeError | xfrm_output_one() |
| linux _ mib _ xfrmoutstateseqerror | XfrmOutStateSeqError | xfrm_output_one() |
| LINUX_MIB_XFRMOUTSTATEEXPIRED | XfrmOutStateExpired | xfrm_output_one() |
| LINUX_MIB_XFRMOUTPOLBLOCK | XfrmOutPolBlock | xfrm_lookup() |
| LINUX_MIB_XFRMOUTPOLDEAD | XfrmOutPolDead | n/a |
| LINUX_MIB_XFRMOUTPOLERROR | XfrmOutPolError | xfrm_bundle_lookup()xfrm_resolve_and_create_bundle() |
| Linux _ MIB _ xfrmfwdhdreerror | XfrmFwdHdrError | __xfrm_route_forward() |
| linux _ mib _ xfrmoutstateinvalid | XfrmOutStateInvalid | xfrm_output_one() |

image IPsec git 树:git://git . kernel . org/pub/SCM/Linux/kernel/git/klassert/IPsec . git

ipsec git 树用于修复 ipsec 网络子系统;这个树中的开发是针对 David Miller 的 net git 树完成的。

ipsec-next git 树:git://git . kernel . org/pub/SCM/Linux/kernel/git/klass et/IPSec-next . git

ipsec-next 树用于以 linux-next 为目标的 ipsec 更改;这个树的开发是针对 David Miller 的 net-next git 树进行的。

IPsec 子系统维护人员是 Steffen Klassert、Herbert Xu 和 David S. Miller。

十一、第四层协议

第十章讨论了 Linux IPsec 子系统及其实现。在本章中,我将讨论四种传输层(L4)协议。我将从两个最常用的传输层(L4)协议开始我们的讨论,这两个协议是用户数据报协议(UDP)和传输控制协议(TCP),它们已经使用了很多年。随后,我将讨论较新的流控制传输协议(SCTP)和数据报拥塞控制协议(DCCP),它们结合了 TCP 和 UDP 的特性。我将从描述套接字 API 开始这一章,它是传输层(L4)和用户空间之间的接口。我将讨论套接字如何在内核中实现,以及数据如何从用户空间流向传输层,以及如何从传输层流向用户空间。在使用这些协议时,我还将处理从网络层(L3)到传输层(L4)的数据包传递。我将在这里主要讨论这四个协议的 IPv4 实现,尽管有些代码是 IPv4 和 IPv6 共有的。

套接字

每个操作系统都必须为其网络子系统提供一个入口点和一个 API。Linux 内核网络子系统通过标准的 POSIX 套接字 API 提供到用户空间的接口,该 API 由 IEEE (IEEE Std 1003.1g-2000,描述网络 API,也称为 POSIX.1g)指定。这个 API 基于 Berkeley sockets API(也称为 BSD sockets),它起源于 4.2BSD Unix 操作系统,是几个操作系统中的行业标准。在 Linux 中,传输层以上的一切都属于用户空间。遵循 Unix 范式“一切都是文件”,套接字与文件相关联,这一点您将在本章后面看到。使用统一套接字 API 使得移植应用更加容易。以下是可用的套接字类型:

  • 套接字(SOCK_STREAM): 提供了一个可靠的、字节流的通信通道。TCP 套接字是流套接字的一个例子。
  • 数据报 套接字(SOCK_DGRAM): 提供消息的交换(称为数据报)。数据报套接字提供了一个不可靠的通信通道,因为数据包可能会被丢弃、无序到达或被复制。UDP 套接字是数据报套接字的一个例子。
  • **Raw****sockets(SOCK _ Raw)😗*使用对 IP 层的直接访问,并允许发送或接收流量,而无需任何协议特定的传输层格式化。
  • 可靠传递消息 (SOCK_RDM): 用于透明进程间通信(TIPC),最初由爱立信于 1996 年至 2005 年开发,用于集群应用。参见http://tipc.sourceforge.net
  • 有序的包流**(SOCK _ seq packet)😗*这个套接字类型类似于 SOCK_STREAM 类型,也是面向连接的。这两种类型之间的唯一区别是使用 SOCK_SEQPACKET 类型维护记录边界。通过 MSG_EOR(记录结束)标志,接收器可以看到记录边界。本章不讨论有序数据包流类型。
  • DCCP 套接字 (SOCK_DCCP): 数据报拥塞控制协议是一种传输协议,提供不可靠数据报的拥塞控制流。它结合了 TCP 和 UDP 的功能。这将在本章的后一节讨论。
  • 数据链接套接字**(SOCK _ PACKET)😗*SOCK _ PACKET 在 AF_INET 家族中被认为是过时的。参见net/socket.c中的__sock_create()方法。

下面是 sockets API 提供的一些方法的描述(下面列表中出现的所有内核方法都是在net/socket.c中实现的):

  • socket() :新建一个套接字;将在“创建套接字”小节中讨论
  • bind() :将套接字与本地端口和 IP 地址关联;通过sys_bind()方法在内核中实现。
  • send() :发送消息;通过sys_send()方法在内核中实现。
  • recv() :接收消息;通过sys_recv()方法在内核中实现。
  • listen() :允许一个套接字接收来自其他套接字的连接;通过sys_listen()方法在内核中实现。与数据报套接字无关。
  • accept() :接受套接字上的连接;通过sys_accept()方法在内核中实现。仅与基于连接的套接字类型相关(SOCK_STREAM、SOCK_SEQPACKET)。
  • connect() :建立到对等套接字的连接;通过sys_connect()方法在内核中实现。与基于连接的套接字类型(SOCK_STREAM 或 SOCK_SEQPACKET)以及无连接套接字类型(SOCK_DGRAM)相关。

本书重点介绍内核网络实现,所以我就不深究用户空间 socket API 的细节了。如果你想了解更多信息,我推荐以下书籍:

  • 由 W. Richard Stevens、Bill Fenner 和 Andrew m . Rudoff(Addison-Wesley Professional,2003 年)编写的《Unix 网络编程,第 1 卷:套接字网络 API(第 3 版)》。
  • 《Linux 编程接口》,作者 Michael Kerrisk(无淀粉出版社,2010 年)。

image 注意所有的 socket API 调用都由socketcall()方法处理,在net/socket.c中。

现在,您已经了解了一些套接字类型,您将了解创建套接字时内核中会发生什么。在下一节中,我将介绍实现套接字的两种结构:struct socketstruct sock。我还将描述它们之间的区别,我将描述msghdr struct及其成员。

创建套接字

内核中有两种结构代表一个套接字:第一种是struct socket ,它提供了一个到用户空间的接口,由sys_socket()方法创建。我将在本节稍后讨论sys_socket()方法。第二个是struct sock ,提供了到网络层(L3)的接口。因为sock结构驻留在网络层,所以它是一个协议不可知的结构。我将在本节稍后讨论sock结构。socket结构较短:

struct socket {
        socket_state            state;

        kmemcheck_bitfield_begin(type);
        short                   type;
        kmemcheck_bitfield_end(type);

        unsigned long           flags;

        . . .

        struct file             *file;
        struct sock             *sk;
        const struct proto_ops  *ops;
};

(我nclude/linux/net.h)

以下是对socket结构成员的描述:

  • state :套接字可以有几种状态,比如 SS_UNCONNECTED、SS_CONNECTED 等等。创建 INET 套接字时,其状态为 SS _ UNCONNECTED 见inet_create()法。流套接字成功连接到另一台主机后,其状态为 SS_CONNECTED。参见include/uapi/linux/net.h中的socket_state enum
  • type :套接字的类型,如 SOCK_STREAM 或 SOCK _ RAW 参见include/linux/net.h中的enum sock_type
  • flags :套接字标志;例如,SOCK_EXTERNALLY_ALLOCATED 标志是在分配套接字时在 TUN 设备中设置的,而不是由socket()系统调用设置的。参见drivers/net/tun.c中的tun_chr_open()方法。套接字标志在include/linux/net.h中定义。
  • file :与套接字关联的文件。
  • sk :与套接字关联的sock对象。sock对象代表网络层(L3)的接口。创建套接字时,会创建相关的sk对象。例如,在 IPv4 中,创建套接字时调用的inet_create()方法分配一个sock对象sk,并将其与指定的套接字对象相关联。
  • ops :这个对象(proto_ops对象的一个实例)主要由这个套接字的回调组成,比如connect()listen()sendmsg()recvmsg()等等。这些回调是用户空间的接口。sendmsg()回调实现了几个库级例程,比如write()send()sendto()sendmsg()。非常相似的是,recvmsg()回调实现了几个库级例程,比如read()recv()recvfrom()recvmsg()。每个协议根据协议要求定义一个自己的proto_ops对象。因此,对于 TCP,它的proto_ops对象包括一个listen回调、inet_listen()和一个accept回调、inet_accept()。另一方面,在客户机-服务器模型中不工作的 UDP 协议将listen()回调定义为sock_no_listen()方法,并将accept()回调定义为sock_no_accept()方法。这两种方法唯一做的事情是返回错误–EOPNOTSUPP。有关 TCP 和 UDP proto_ops对象的定义,请参见本章末尾“快速参考”部分的表 11-1 。proto_ops结构在include/linux/net.h中定义。

sock结构是套接字的网络层表示;它很长,以下是对我们的讨论很重要的一些字段:

struct sock {

        struct sk_buff_head     sk_receive_queue;
        int                     sk_rcvbuf;

        unsigned long           sk_flags;

        int                     sk_sndbuf;
        struct sk_buff_head     sk_write_queue;
        . . .
        unsigned int            sk_shutdown  : 2,
                                sk_no_check  : 2,
                                sk_protocol  : 8,
                                sk_type      : 16;
        . . .

        void                    (*sk_data_ready)(struct sock *sk, int bytes);
        void                    (*sk_write_space)(struct sock *sk);
};
(include/net/sock.h)

以下是对sock结构成员的描述:

  • sk_receive_queue:输入数据包的队列。
  • sk_rcvbuf:接收缓冲区的大小,以字节为单位。
  • sk_flags:各种旗帜,像 SOCK_DEAD 或者 SOCK _ DEAD 参见include/net/sock.h中的sock_flags enum定义。
  • sk_sndbuf:发送缓冲区的大小,以字节为单位。
  • sk_write_queue:输出数据包的队列。

image 注意稍后,在“TCP 套接字初始化”部分,您将看到sk_rcvbufsk_sndbuf是如何初始化的,以及如何通过写入procfs条目来改变。

  • sk_no_check:禁用校验和标志。可以用 SO_NO_CHECK 套接字选项设置。
  • sk_protocol:协议标识,根据socket()系统调用的第三个参数(protocol)设置。
  • sk_type:套接字的类型,如 SOCK_STREAM 或 SOCK _ RAW 参见include/linux/net.h中的enum sock_type
  • sk_data_ready:通知套接字新数据已经到达的回调。
  • sk_write_space:回调,表示有空闲内存可以进行数据传输。

创建套接字是通过从用户空间调用socket()系统调用来完成的:

sockfd = socket(int socket_family, int socket_type, int protocol);

下面是对socket()系统调用的参数描述:

  • socket_family:例如,可以是 IPv4 的 AF_INET、IPv6 的 AF_INET6 或 UNIX 域套接字的 AF_UNIX 等。(UNIX 域套接字是进程间通信(IPC)的一种形式,它允许在同一主机上运行的进程之间进行通信。)

  • socket_type:例如,可以是流套接字的 SOCK_STREAM、数据报套接字的 SOCK_DGRAM 或原始套接字的 SOCK_RAW 等等。

  • protocol:可以是以下任意一种:

  • 对于 TCP 套接字,为 0 或 IPPROTO_TCP。

  • 0 或 IPPROTO_UDP 用于 UDP 套接字。

  • 原始套接字的有效 IP 协议标识符(如 IPPROTO_TCP 或 IP proto _ ICMP);参见 RFC 1700,“分配的号码”

socket()系统调用(sockfd)的返回值是文件描述符,它应该作为参数传递给这个套接字的后续调用。socket()系统调用在内核中由sys_socket()方法处理。让我们来看看socket()系统调用的实现:

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
        int retval;
        struct socket *sock;
        int flags;

        . . .
        retval = sock_create(family, type, protocol, &sock);
        if (retval < 0)
                goto out;
        . . .
        retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
        if (retval < 0)
                goto out_release;
out:
        . . .
        return retval;

}
(net/socket.c)

sock_create()方法调用特定于地址族的套接字创建方法create();在 IPv4 的情况下,它是inet_create()方法。(参见net/ipv4/af_inet.c中的inet_family_ops定义。)方法inet_create()创建与套接字关联的sock对象(sk);sock对象代表网络层套接字接口。sock_map_fd()方法返回一个与套接字关联的fd(文件描述符);通常情况下,socket()系统调用会返回这个fd

从用户空间套接字发送数据,或者在用户空间套接字中从传输层接收数据,在内核中分别由sendmsg()recvmsg()方法处理,这两个方法获得一个msghdr对象作为参数。msghdr对象包括要发送或填充的数据块,以及一些其他参数。

struct msghdr {
        void             *msg_name;       /* Socket name                                         */
        int              msg_namelen;     /* Length of name                                      */
        struct iovec     *msg_iov;        /* Data blocks                                         */
        __kernel_size_t  msg_iovlen;      /* Number of blocks                                    */
        void             *msg_control;    /* Per protocol magic (eg BSD file descriptor passing) */
        __kernel_size_t  msg_controllen;  /* Length of cmsg list                                 */
        unsigned int     msg_flags;
};
(include/linux/socket.h)

以下是对msghdr结构 中一些重要成员的描述:

  • msg_name:目的套接字地址。为了得到目标套接字,通常将msg_name不透明指针转换为struct sockaddr_in指针。例如,参见udp_sendmsg()方法。
  • msg_namelen:地址的长度。
  • iovec:数据块的向量。
  • msg_iovlen:矢量iovec中的块数。
  • msg_control:控制信息(又称辅助数据)。
  • msg_controllen:控制信息的长度。
  • msg_flags:收到消息的标志,如 MSG_MORE。(例如,请参阅本章后面的“使用 UDP 发送数据包”一节。)

注意,内核可以处理的最大控制缓冲区长度受sysctl_optmem_max ( /proc/sys/net/core/optmem_max)中值的限制。

在本节中,我描述了在发送和接收数据包时使用的socketmsghdr struct的内核实现。在下一节中,我将从描述 UDP 协议开始讨论传输层协议(L4 ), UDP 协议是本章要讨论的协议中最简单的一种。

用户数据报协议

UDP 协议在 1980 年的 RFC 768 中被描述。UDP 协议是 IP 层周围的一个薄层,仅添加端口、长度和校验和信息。它可以追溯到 1980 年,提供不可靠的、面向消息的传输,没有拥塞控制。许多协议都使用 UDP。例如,我将提到 RTP 协议(实时传输协议,它用于通过 IP 网络传输音频和视频。这种类型的流量可以容忍一些数据包丢失。RTP 通常用于 VoIP 应用中,通常与基于 SIP(会话发起协议)的客户端结合使用。(这里需要提到的是,其实 RTP 协议也可以使用 TCP,RFC 4571 中有规定,但是这个用的不多。)这里我应该提一下 UDP-Lite,它是 UDP 协议的扩展,支持可变长度校验和(RFC 3828)。大多数 UDP-Lite 是在net/ipv4/udplite.c中实现的,但是您也会在主 UDP 模块net/ipv4/udp.c中遇到它。UDP 报头长度为 8 个字节:

struct udphdr {
        __be16  source;
        __be16  dest;
        __be16  len;
        __sum16 check;
};
(include/uapi/linux/udp.h)

以下是对 UDP 报头成员的描述:

  • source:源端口(16 位),范围 1-65535。
  • dest:目的端口(16 位),范围 1-65535。
  • len:字节长度(有效载荷长度和 UDP 头长度)。
  • checksum:数据包的校验和。

图 11-1 显示了一个 UDP 头。

9781430261964_Fig11-01.jpg

图 11-1 。UDP 报头(IPv4)

在本节中,您了解了 UDP 头及其成员。为了理解使用 sockets API 的用户空间应用如何与内核通信(发送和接收数据包),您应该知道 UDP 初始化是如何完成的,这将在下一节中描述。

UDP 初始化

我们定义了udp_protocol对象(net_protocol对象)并用inet_add_protocol()方法添加它。这将udp_protocol对象设置为全局协议数组(inet_protos)中的一个元素。

static const struct net_protocol udp_protocol = {
        .handler =      udp_rcv,
        .err_handler =  udp_err,
        .no_policy =    1,
        .netns_ok =     1,
};
(net/ipv4/af_inet.c)

static int __init inet_init(void)
{
        . . .
        if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
                pr_crit("%s: Cannot add UDP protocol\n", __func__);
        . . .
}
(net/ipv4/af_inet.c)

我们进一步定义了一个udp_prot对象,并通过调用proto_register()方法注册它。该对象主要包含回调;当在用户空间中打开 UDP 套接字并使用套接字 API 时,会调用这些回调。例如,在 UDP 套接字上调用setsockopt()系统调用将调用udp_setsockopt()回调。

struct proto udp_prot = {
         .name              = "UDP",
         .owner             = THIS_MODULE,
         .close             = udp_lib_close,
         .connect           = ip4_datagram_connect,
         .disconnect        = udp_disconnect,
         .ioctl             = udp_ioctl,
         . . .
         .setsockopt        = udp_setsockopt,
         .getsockopt        = udp_getsockopt,
         .sendmsg           = udp_sendmsg,
         .recvmsg           = udp_recvmsg,
         .sendpage          = udp_sendpage,
         . . .
};

(net/ipv4/udp.c)
int __init inet_init(void)
{
    int rc = -EINVAL;
    . . .
    rc = proto_register(&udp_prot, 1);
    . . .

}
(net/ipv4/af_inet.c)

image 注意UDP 协议和其他核心协议在启动时通过inet_init()方法初始化。

既然您已经了解了 UDP 初始化及其用于发送数据包的回调,也就是本节中显示的udp_prot对象的udp_sendmsg()回调,那么是时候了解 UDP 如何在 IPV4 中发送数据包了。

使用 UDP 发送数据包

从 UDP 用户空间套接字发送数据可以通过几个系统调用来完成:send()sendto()sendmsg()write();最终它们都由内核中的udp_sendmsg()方法处理。用户空间应用构建一个包含数据块的msghdr对象,并将这个msghdr对象传递给内核。让我们来看看这个方法:

int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
                size_t len)
{

通常,UDP 数据包会立即发送。这种行为可以通过 UDP_CORK socket 选项(在内核 2.5.44 中引入)来改变,它会导致传递给udp_sendmsg()方法的数据包数据不断累积,直到通过取消设置该选项来释放最后一个数据包。通过设置 MSG_MORE 标志可以获得相同的结果:

        int corkreq = up->corkflag || msg->msg_flags&MSG_MORE;
        struct inet_sock *inet = inet_sk(sk);
              . . .

首先,我们做一些理智检查。例如,指定的len不能大于 65535(记住 UDP 头中的len字段是 16 位):

        if (len > 0xFFFF)
                 return -EMSGSIZE;

我们需要知道目的地地址和目的地端口,以便构建一个flowi4对象,这是用udp_send_skb()方法或ip_append_data()方法发送 SKB 所需要的。目标端口不应为 0。这里有两种情况:目的地在msghdrmsg_name中被指定,或者套接字被连接,其状态为 TCP_ESTABLISHED。请注意,UDP(与 TCP 相反)几乎是一种完全无状态的协议。UDP 中 TCP_ESTABLISHED 的概念主要意味着套接字已经通过了一些健全性检查。

        if (msg->msg_name) {
                struct sockaddr_in *usin = (struct sockaddr_in *)msg->msg_name;
                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;

Linux 代码承认 IANA 没有保留任何 UDP/TCP 端口。TCP 和 UDP 中端口 0 的保留可以追溯到 RFC 1010,“分配的号码”(1987),并且它仍然存在于 RFC 1700 中,该 RFC 1700 已被在线数据库淘汰(参见 RFC 3232),它们仍然存在于 RFC 1700 中。参见www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml

                if (dport == 0)
                        return -EINVAL;
        } else {
                if (sk->sk_state != TCP_ESTABLISHED)
                        return -EDESTADDRREQ;
                daddr = inet->inet_daddr;
                dport = inet->inet_dport;
                /* Open fast path for connected socket.
                   Route will not be used, if at least one option is set.
                 */
                connected = 1;
}

               . . .

用户空间应用可以通过在msghdr对象中设置msg_controlmsg_controllen来发送控制信息(也称为辅助数据)。辅助数据实际上是一系列带有附加数据的cmsghdr对象。(更多细节见man 3 cmsg。)您可以通过分别调用sendmsg()recvmsg()方法来发送和接收辅助数据。例如,您可以创建 IP_PKTINFO 辅助消息来设置到未连接的 UDP 套接字的源路由。(参见man 7 ip。)当msg_controllen不为 0 时,这是一个控制信息消息,由ip_cmsg_send()方法处理。ip_cmsg_send()方法通过解析指定的msghdr对象构建一个ipcm_cookie (IP 控制消息 Cookie)对象。ipcm_cookie结构包括在处理数据包时进一步使用的信息。例如,当使用 IP_PKTINFO 辅助消息时,您可以通过设置控制消息中的地址字段来设置源地址,这最终会设置ipcm_cookie对象中的addripcm_cookie是一个短结构:

struct ipcm_cookie {
        __be32                  addr;
        int                     oif;
        struct ip_options_rcu   *opt;
        __u8                    tx_flags;
};
(include/net/ip.h)

让我们继续讨论 udp_sendmsg()方法:

         if (msg->msg_controllen) {
                 err = ip_cmsg_send(sock_net(sk), msg, &ipc);
                 if (err)
                         return err;
                 if (ipc.opt)
                         free = 1;
                 connected = 0;
         }
         . . .
         if (connected)
                 rt = (struct rtable *)sk_dst_check(sk, 0);
         . . .

如果路由条目为空,则应执行路由查找:

        if (rt == NULL) {
                struct net *net = sock_net(sk);
                fl4 = &fl4_stack;
                flowi4_init_output(fl4, ipc.oif, sk->sk_mark, tos,
                                   RT_SCOPE_UNIVERSE, sk->sk_protocol,
                                   inet_sk_flowi_flags(sk)|FLOWI_FLAG_CAN_SLEEP,
                                   faddr, saddr, dport, inet->inet_sport);

                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_BH(net, IPSTATS_MIB_OUTNOROUTES);
                        goto out;
                }

        . . .

在内核 2.6.39 中,增加了无锁传输快速路径。这意味着当 corking 特性未设置时,我们不持有套接字锁,而是调用udp_send_skb()方法,当 corking 特性设置时,我们通过调用lock_sock()方法持有套接字锁,然后发送数据包:

        /* Lockless fast path for the non-corking case. */
if (!corkreq) {
                skb = ip_make_skb(sk, fl4, getfrag, msg->msg_iov, ulen,
                                  sizeof(struct udphdr), &ipc, &rt,
                                  msg->msg_flags);
                err = PTR_ERR(skb);
                if (!IS_ERR_OR_NULL(skb))
                         err = udp_send_skb(skb, fl4);
                 goto out;
        }

现在我们处理设置了软木塞特征的情况:

       lock_sock(sk);
do_append_data:
        up->len += ulen;

ip_append_data()方法缓冲要传输的数据,但不传输它。随后调用udp_push_pending_frames()方法将实际执行传输。注意,udp_push_pending_frames()方法也通过指定的getfrag回调来处理碎片:

        err = ip_append_data(sk, fl4, getfrag, msg->msg_iov, ulen,
                             sizeof(struct udphdr), &ipc, &rt,
                             corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);

如果该方法失败,我们应该刷新所有挂起的 skb。这是通过调用udp_flush_pending_frames()方法实现的,该方法将通过ip_flush_pending_frames()方法释放套接字(sk_write_queue)的写队列中的所有 skb:

        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);

在本节中,您了解了如何使用 UDP 发送数据包。现在,为了完成我们对 IPv4 中 UDP 的讨论,我们应该了解一下 IPv4 中的 UDP 是如何接收来自网络层(L3)的数据包的。

使用 UDP 从网络层(L3)接收数据包

从网络层(L3)接收 UDP 包的主要处理程序是udp_rcv()方法。它只是调用了__udp4_lib_rcv()方法(net/ipv4/udp.c):

int udp_rcv(struct sk_buff *skb)
{
        return __udp4_lib_rcv(skb, &udp_table, IPPROTO_UDP);
}

我们来看看__udp4_lib_rcv()方法:

int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable,
                   int proto)
{
        struct sock *sk;
        struct udphdr *uh;
        unsigned short ulen;
        struct rtable *rt = skb_rtable(skb);
        __be32 saddr, daddr;
        struct net *net = dev_net(skb->dev);
        . . .

我们从 SKB 获取 UDP 报头、报头长度以及源地址和目的地址:

        uh   = udp_hdr(skb);
        ulen = ntohs(uh->len);
        saddr = ip_hdr(skb)->saddr;
        daddr = ip_hdr(skb)->daddr;

我们将跳过一些正在执行的健全性检查,比如确保 UDP 报头长度不大于数据包的长度,以及指定的proto是 UDP 协议标识符(IPPROTO_UDP)。如果数据包是广播或组播数据包,将通过__udp4_lib_mcast_deliver()方法进行处理:

        if (rt->rt_flags & (RTCF_BROADCAST|RTCF_MULTICAST))
             return __udp4_lib_mcast_deliver(net, skb, uh,
                                                saddr, daddr, udptable);

接下来,我们在 UDP 套接字哈希表中执行查找:

        sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);
              if (sk != NULL) {

我们到达这里是因为我们执行的查找找到了一个匹配的套接字。因此,通过调用udp_queue_rcv_skb()方法进一步处理 SKB,该方法调用通用的sock_queue_rcv_skb()方法,该方法又将指定的 SKB 添加到sk->sk_receive_queue的尾部(通过调用__skb_queue_tail()方法):

        int ret = udp_queue_rcv_skb(sk, skb);
        sock_put(sk);

        /* a return value > 0 means to resubmit the input, but
        * it wants the return to be -protocol, or 0
        */
        if (ret > 0)
             return -ret;

一切都好;返回 0 表示成功:

            return 0;
        }
        . . .

我们来到这里是因为查找套接字失败了。这意味着我们不应该处理数据包。例如,当目的端口上没有监听 UDP 套接字时,就会发生这种情况。如果校验和不正确,我们应该悄悄地丢弃数据包。如果它是正确的,我们应该发送一个 ICMP 回复给发送者。这应该是一个 ICMP 消息“目的地不可达”,代码为“端口不可达”接下来,我们应该释放数据包并更新 SNMP MIB 计数器:

        /* No socket. Drop packet silently, if checksum is wrong */
        if (udp_lib_checksum_complete(skb))
            goto csum_error;

下一个命令递增 UDP_MIB_NOPORTS ( NoPorts ) MIB 计数器。请注意,您可以通过cat /proc/net/snmpnetstat –s查询各种 UDP MIB 计数器。

UDP_INC_STATS_BH(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;

图 11-2 展示了我们在本节中关于接收 UDP 数据包的讨论。

9781430261964_Fig11-02.jpg

图 11-2 。接收 UDP 数据包

我们关于 UDP 的讨论到此结束。下一节描述 TCP 协议,它是本章讨论的协议中最复杂的。

TCP (传输控制协议)

TCP 协议在 1981 年的 RFC 793 中被描述为 。从那以后的几年里,基本的 TCP 协议有了许多更新、变化和补充。一些增加是针对特定类型的网络(高速、卫星),而另一些是为了提高性能。

TCP 协议是当今互联网上最常用的传输协议。许多众所周知的协议都是基于 TCP 的。最广为人知的协议大概就是 HTTP 了,这里还要提一下其他一些广为人知的协议比如ftpsshtelnetsmtpssl。与 UDP 相反,TCP 协议提供可靠的面向连接的传输。通过使用序列号和确认,传输变得可靠。

TCP 是一个非常复杂的协议;我们不会在本章中讨论 TCP 实现的所有细节、优化和细微差别,因为这本身就需要一本单独的书。TCP 功能由两部分组成:连接管理和数据传输与接收。在这一节中,我们将重点介绍 TCP 初始化和 TCP 连接设置,这属于第一个要素,即连接管理,以及接收和发送数据包,这属于第二个要素。这些是进一步深入研究 TCP 协议实现的重要基础。我们应该注意到,TCP 协议通过拥塞控制来自我调节字节流。已经规定了许多不同的拥塞控制算法,Linux 提供了一个可插入和可配置的架构来支持各种各样的算法。深入研究单个拥塞控制算法的细节超出了本书的范围。

每个 TCP 数据包都以 TCP 报头开始。为了理解 TCP 的操作,您必须了解 TCP 报头。下一节描述 IPv4 TCP 报头。

TCP 报头

TCP 报头长度为 20 字节,但在使用 TCP 选项时可扩展至 60 字节:

struct tcphdr {
        __be16  source;
        __be16  dest;
        __be32  seq;
        __be32  ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)
        __u16   res1:4,
                doff:4,
                fin:1,
                syn:1,
                rst:1,
                psh:1,
                ack:1,
                urg:1,
                ece:1,
                cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
        __u16   doff:4,
                res1:4,
                cwr:1,
                ece:1,
                urg:1,
                ack:1,
                psh:1,
                rst:1,
                syn:1,
                fin:1;
#else
#error  "Adjust your <asm/byteorder.h> defines"
#endif
        __be16  window;
        __sum16 check;
        __be16  urg_ptr;
};
(include/uapi/linux/tcp.h)

以下是对tcphdr结构成员的描述:

  • source:源端口(16 位),范围 1-65535。
  • dest:目的端口(16 位),范围 1-65535。
  • seq:序列号(32 位)。
  • ack_seq:确认号(32 位)。如果 ACK 标志被置位,该域的值就是接收方所期望的下一个序列号。
  • res1:留作将来使用(4 位)。它应该总是设置为 0。
  • doff:数据偏移(4 位)。TCP 报头的大小乘以 4 个字节;最小值为 5 (20 字节),最大值为 15 (60 字节)。

以下是 TCP 标志;每个都是 1 位:

  • fin:没有来自发送方的更多数据(当一个端点想要关闭连接时)。
  • syn:SYN 标志最初是在两个端点之间建立 3 次握手时发送的。
  • rst:当不是用于当前连接的段到达时,使用复位标志。
  • psh:数据应该尽快传递到用户空间。
  • ack:表示 TCP 报头中的确认号(ack_seq)值是有意义的。
  • urg:表示紧急指针有意义。
  • ece : ECN - Echo 标志。 ECN 代表“显式拥塞通知”ECN 提供了一种机制,可以在不丢弃数据包的情况下发送有关网络拥塞的端到端通知。它是由 RFC 3168 于 2001 年提出的“对 IP 的显式拥塞通知(ECN)的添加”。
  • cwr:拥塞窗口减少标志。
  • window : TCP 接收窗口大小,以字节为单位(16 位)。
  • check:TCP 报头和 TCP 数据的校验和。
  • urg_ptr:仅当urg标志被置位时才有意义。它表示相对于序列号的偏移量,表示最后一个紧急数据字节(16 位)。

图 11-3 显示了一个 TCP 报头的示意图。

9781430261964_Fig11-03.jpg

图 11-3 。TCP 报头(IPv4)

在本节中,我描述了 IPv4 TCP 报头及其成员。您可以看到,与只有 4 个成员的 UDP 报头相比,TCP 报头有更多的成员,因为 TCP 是一种复杂得多的协议。在下一节中,我将描述 TCP 初始化是如何完成的,这样您将了解接收和发送 TCP 包的回调的初始化是如何发生的以及在哪里发生的。

TCP 初始化

我们定义了tcp_protocol对象(net_protocol对象)并用inet_add_protocol()方法添加它:

static const struct net_protocol tcp_protocol = {
        .early_demux    =       tcp_v4_early_demux,
        .handler        =       tcp_v4_rcv,
        .err_handler    =       tcp_v4_err,
        .no_policy      =       1,
        .netns_ok       =       1,
};
(net/ipv4/af_inet.c)

static int __init inet_init(void)
  {
        . . .
        if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
            pr_crit("%s: Cannot add TCP protocol\n", __func__);
        . . .
}
(net/ipv4/af_inet.c)

我们进一步定义了一个tcp_prot对象,并通过调用proto_register()方法来注册它,就像我们对 UDP 所做的那样:

struct proto tcp_prot = {
        .name                   = "TCP",
        .owner                  = THIS_MODULE,
        .close                  = tcp_close,
        .connect                = tcp_v4_connect,
        .disconnect             = tcp_disconnect,
        .accept                 = inet_csk_accept,
        .ioctl                  = tcp_ioctl,
        .init                   = tcp_v4_init_sock,
        . . .
};

(net/ipv4/tcp_ipv4.c)

static int __init inet_init(void)
{
        int rc;
        . . .
        rc = proto_register(&tcp_prot, 1);
        . . .
}
(net/ipv4/af_inet.c)

注意,在tcp_prot定义中,init函数指针被定义为tcp_v4_init_sock()回调,它执行各种初始化,比如通过调用tcp_init_xmit_timers()方法设置定时器、设置套接字状态等等。相反,在简单得多的协议 UDP 中,根本没有定义init函数指针,因为在 UDP 中没有要执行的特殊初始化。我们将在本节稍后讨论tcp_v4_init_sock()回调。

在下一节中,我将简要描述 TCP 协议使用的定时器。

TCP 定时器

TCP 定时器在net/ipv4/tcp_timer.c中处理。TCP 使用四种计时器:

  • 重发定时器 : 负责重新发送在指定时间间隔内未被确认的数据包。当数据包丢失或损坏时,可能会发生这种情况。该定时器在每个数据段发送后启动;如果 ACK 在定时器到期之前到达,则定时器被取消。
  • 延迟 ACK 定时器 : 延迟发送 ACK 包。当 TCP 接收到必须确认但不需要立即确认的数据时,该位置位。
  • 保活定时器 : 检查连接是否断开。有这样的情况,会话长时间闲置,一方宕机。保持活动计时器检测到这种情况,并调用tcp_send_active_reset()方法来重置连接。
  • 零窗口探测定时器 (也称为 持久定时器 ): 当接收缓冲区满时,接收方通告零窗口,发送方停止发送。现在,当接收方发送一个具有新窗口大小的数据段,而该数据段丢失时,发送方将永远等待下去。解决方案是这样的:当发送方得到一个零窗口时,它使用一个持久定时器来探测接收方的窗口大小;当获得非零窗口大小时,持久定时器停止。

TCP 套接字初始化

要使用 TCP 套接字,用户空间应用应该创建一个 SOCK_STREAM 套接字,并调用socket()系统调用。这是在内核中由tcp_v4_init_sock()回调处理的,回调调用tcp_init_sock()方法来完成真正的工作。注意,tcp_init_sock()方法执行独立于地址族的初始化,它也是从tcp_v6_init_sock()方法调用的。tcp_init_sock()方法的重要任务如下:

  • 将套接字的状态设置为 TCP_CLOSE。
  • 通过调用tcp_init_xmit_timers()方法初始化 TCP 定时器。
  • 初始化 socket 发送缓冲区(sk_sndbuf)和接收缓冲区(sk_rcvbuf);sk_sndbuf设置为sysctl_tcp_wmem[1],默认为 16384 字节,sk_rcvbuf设置为sysctl_tcp_rmem[1],默认为 87380 字节。这些默认值在tcp_init()方法中设置;通过分别写入/proc/sys/net/ipv4/tcp_wmem/proc/sys/net/ipv4/tcp_rmem,可以覆盖sysctl_tcp_wmemsysctl_tcp_rmem数组的默认值。参见Documentation/networking/ip-sysctl.txt中的“TCP 变量”部分。
  • 初始化无序队列和prequeue
  • 初始化各种参数。例如,根据 2013 年的 RFC 6928“增加 TCP 的初始窗口”,TCP 初始拥塞窗口被初始化为 10 个段(TCP_INIT_CWND)。

现在您已经了解了 TCP 套接字是如何初始化的,我将讨论如何建立 TCP 连接。

TCP 连接设置

TCP 连接建立和拆除以及 TCP 连接属性被描述为状态机中的转换。在每个给定的时刻,TCP 套接字可以处于一个指定的状态;例如,当调用listen()系统调用时,套接字进入 TCP_LISTEN 状态。对象的状态由它的成员sk_state来表示。有关所有可用状态的列表,请参考include/net/tcp_states.h

三次握手用于在 TCP 客户端和 TCP 服务器之间建立 TCP 连接:

  • 首先,客户端向服务器发送一个 SYN 请求。其状态更改为 TCP_SYN_SENT。
  • 正在监听的服务器套接字(其状态为 TCP_LISTEN)创建一个请求套接字来表示 TCP_SYN_RECV 状态下的新连接,并发回一个 SYN ACK。
  • 接收 SYN ACK 的客户端将其状态更改为 TCP_ESTABLISHED,并向服务器发送 ACK。
  • 服务器接收到 ACK 并将请求套接字更改为 TCP_ESTABLISHED 状态的子套接字,因为现在连接已经建立,可以发送数据了。

image 注意要进一步了解 TCP 状态机的细节,请参考tcp_rcv_state_process()方法(net/ipv4/tcp_input.c),它是状态机引擎,适用于 IPv4 和 IPv6。(它由tcp_v4_do_rcv()方法和tcp_v6_do_rcv()方法调用。)

下一节将描述如何使用 IPv4 中的 TCP 从网络层(L3)接收数据包。

使用 TCP 从网络层(L3)接收 数据包

从网络层(L3)接收 TCP 包的主要处理程序是tcp_v4_rcv()方法(net/ipv4/tcp_ipv4.c)。让我们来看看这个函数:

int tcp_v4_rcv(struct sk_buff *skb)
{
       struct sock *sk;
       . . .

首先,我们进行一些健全性检查(例如,检查数据包类型是否不是 PACKET_HOST,或者数据包大小是否比 TCP 报头短),如果有任何问题,就丢弃数据包;然后进行一些初始化,并通过调用__inet_lookup_skb()方法执行相应套接字的查找,首先通过调用__inet_lookup_established()方法在已建立的套接字散列表中执行查找。在查找失败的情况下,它通过调用__inet_lookup_listener()方法在监听套接字哈希表中执行查找。如果没有找到套接字,则在此阶段丢弃该数据包。

        sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
        . . .
        if (!sk)
                goto no_tcp_socket;

现在我们检查套接字是否属于某个应用。当当前有一个应用拥有该套接字时,sock_owned_by_user()宏返回 1,当没有应用拥有该套接字时,它返回值 0:

        if (!sock_owned_by_user(sk)) {
        . . .
                {

如果没有应用拥有套接字,我们就到达这里,因此它可以接受数据包。首先,我们试图通过调用tcp_prequeue()方法将数据包放入prequeue中,因为在prequeue中处理数据包会更有效。如果prequeue中的处理不可行(例如,当队列没有空间时),则tcp_prequeue()将返回false;在这种情况下,我们将调用tcp_v4_do_rcv()方法,稍后我们将对此进行讨论:

                if (!tcp_prequeue(sk, skb))
                        ret = tcp_v4_do_rcv(sk, skb);
        }

当应用拥有套接字时,这意味着它处于锁定状态,因此它不能接受数据包。在这种情况下,我们通过调用sk_add_backlog()方法将包添加到 backlog 中:

                } else if (unlikely(sk_add_backlog(sk, skb,
                                                   sk->sk_rcvbuf + sk->sk_sndbuf))) {
                        bh_unlock_sock(sk);
                        NET_INC_STATS_BH(net, LINUX_MIB_TCPBACKLOGDROP);
                        goto discard_and_relse;
                }
        }

我们来看看tcp_v4_do_rcv()方法:

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{

如果套接字处于 TCP_ESTABLISHED 状态,我们调用tcp_rcv_established()方法:

        if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
        . . .
                if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {
                        rsk = sk;
                        goto reset;
                }
                return 0;

如果套接字处于 TCP_LISTEN 状态,我们调用tcp_v4_hnd_req()方法:

        if (sk->sk_state == TCP_LISTEN) {
                struct sock *nsk = tcp_v4_hnd_req(sk, skb);

        }

如果我们不在 TCP_LISTEN 状态,我们调用tcp_rcv_state_process()方法:

        if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {
                rsk = sk;
                goto reset;
        }
        return 0;

reset:
        tcp_v4_send_reset(rsk, skb);

}

在本节中,您了解了 TCP 数据包的接收。在下一节中,我们将通过描述在 IPv4 中如何使用 TCP 发送数据包来结束本章的 TCP 部分。

使用 TCP 发送个数据包

与 UDP 一样,从用户空间中创建的 TCP 套接字发送数据包可以通过几个系统调用来完成:send()sendto()sendmsg()write()。最终它们都被tcp_sendmsg()方法(net/ipv4/tcp.c)处理。这个方法将有效负载从用户空间复制到内核,并作为 TCP 段发送。它比udp_sendmsg()方法复杂得多。

int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
                size_t size)
{
        struct iovec *iov;
        struct tcp_sock *tp = tcp_sk(sk);
        struct sk_buff *skb;
        int iovlen, flags, err, copied = 0;
        int mss_now = 0, size_goal, copied_syn = 0, offset = 0;
        bool sg;
        long timeo;
        . . .

我不会深入研究用这种方法将数据从用户空间复制到 SKB 的所有细节。一旦构建了 SKB,它就用调用tcp_write_xmit()方法的tcp_push_one()方法发送,而tcp_write_xmit()方法又调用tcp_transmit_skb()方法:

static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
                            gfp_t gfp_mask)
{

icsk_af_ops对象(INET 连接套接字 ops)是一个特定于地址族的对象。在 IPv4 TCP 的情况下,它在tcp_v4_init_sock()方法中被设置为一个名为ipv4_specificinet_connection_sock_af_ops对象。queue_xmit()回调被设置为通用的ip_queue_xmit()方法。参见net/ipv4/tcp_ipv4.c

    . . .
    err = icsk->icsk_af_ops->queue_xmit(skb, &inet->cork.fl);
    . . .
}
(net/ipv4/tcp_output.c)

现在,您已经了解了 TCP 和 UDP,可以开始下一节了,这一节将讨论 SCTP(流控制传输协议)协议。SCTP 协议结合了 UDP 和 TCP 的特性,比它们都新。

SCTP (流控制传输协议)

SCTP 协议是在 2007 年的 RFC 4960 中指定的。它在 2000 年首次被指定。它是为 IP 网络上的公共交换电话网(PSTN)信令而设计的,但也可用于其他应用。IETF SIGTRAN(信令传输)工作组最初开发了 SCTP 协议,后来将该协议移交给传输区域工作组(TSVWG ),以使 SCTP 继续发展为通用传输协议。LTE(长期演进)使用 SCTP;其中一个主要原因是 SCTP 协议能够检测到链路何时断开或数据包何时被快速丢弃,而 TCP 不具备这一功能。TCP 和 SCTP 中的 SCTP 流量控制和拥塞控制算法非常相似。SCTP 协议对通告的接收器窗口大小使用变量(a_rwnd);该变量表示接收器缓冲区中的当前可用空间。如果接收方指示a_rwnd为 0(没有可用的接收空间),则发送方无法发送任何新数据。SCTP 的重要特征如下:

  • SCTP 结合了 TCP 和 UDP 的功能。它是一种像 TCP 一样具有拥塞控制的可靠传输协议;它像 UDP 一样是面向消息的协议,而 TCP 是面向流的。
  • SCTP 协议通过其 4 次握手(与 TCP 3 次握手相比)提供了改进的安全性,以防止 SYN 泛洪攻击。我将在本章后面的“建立 SCTP 协会”一节中讨论 4 次握手。
  • SCTP 支持多宿主,即两个端点上都有多个 IP 地址。这提供了网络级的容错能力。我将在本节稍后讨论 SCTP 组块。
  • SCTP 支持多流,这意味着它可以并行发送数据块流。在某些环境中,这可以减少流式多媒体的延迟。我将在本节稍后讨论 SCTP 组块。
  • 在多宿主的情况下,SCTP 使用心跳机制来检测空闲/不可到达的对等体。我将在本章后面讨论 SCTP 心跳机制。

在简短描述了 SCTP 协议之后,我们现在将讨论 SCTP 初始化是如何完成的。sctp_init()方法为各种结构分配内存,初始化一些sysctl变量,在 IPv4 和 IPv6 中注册 SCTP 协议:

int sctp_init(void)
{
       int status = -EINVAL;
        . . .
        status = sctp_v4_add_protocol();

       if (status)
               goto err_add_protocol;

       /* Register SCTP with inet6 layer.  */
       status = sctp_v6_add_protocol();
       if (status)
               goto err_v6_add_protocol;
       . . .
}
(net/sctp/protocol.c)

SCTP 协议的注册是通过定义net_protocol的实例(IPv4 的实例名为sctp_protocol,IPv6 的实例名为sctpv6_protocol并调用inet_add_protocol()方法来完成的,这与您在 UDP 协议等其他传输协议中看到的非常相似。我们还调用register_inetaddr_notifier()来接收关于添加或删除网络地址的通知。这些事件将由sctp_inetaddr_event()方法处理,该方法将相应地更新 SCTP 全球地址列表(sctp_local_addr_list)。

static const struct net_protocol sctp_protocol = {
        .handler     = sctp_rcv,
        .err_handler = sctp_v4_err,
        .no_policy   = 1,
};
(net/sctp/protocol.c)

static int sctp_v4_add_protocol(void)
{
        /* Register notifier for inet address additions/deletions. */
        register_inetaddr_notifier(&sctp_inetaddr_notifier);

        /* Register SCTP with inet layer.  */
        if (inet_add_protocol(&sctp_protocol, IPPROTO_SCTP) < 0)
                return -EAGAIN;

        return 0;
}
(net/sctp/protocol.c)

image sctp_v6_add_protocol()的方法(net/sctp/ipv6.c)很相似,这里就不赘述了。

每个 SCTP 数据包都以 SCTP 报头开始。我现在将描述 SCTP 报头的结构。在下一节中,我将与 SCTP·肯斯展开讨论。

SCTP 包和块

每个 SCTP 数据包都有一个 SCTP 公共报头,其后是一个或多个数据块。每个块可以包含数据或 SCTP 控制信息。几个数据块可以打包成一个 SCTP 数据包(建立和终止连接时使用的三个数据块除外:INIT、INIT_ACK 和 SHUTDOWN_COMPLETE)。这些块使用类型-长度-值(TLV)格式,您在第二章中第一次遇到这种格式。

SCTP 普通表头

typedef struct sctphdr {
        __be16 source;
        __be16 dest;
        __be32 vtag;
        __le32 checksum;
} __attribute__((packed)) sctp_sctphdr_t;
(include/linux/sctp.h)

以下是对sctphdr结构成员的描述:

  • sourceSCTP 来源连接埠。
  • dest : SCTP destination port。
  • vtag:验证标签,32 位随机值。
  • checksum:SCTP 公共头和所有块的校验和。

SCTP 块头

SCTP 块头由struct sctp_chunkhdr表示:

typedef struct sctp_chunkhdr {
        __u8 type;
        __u8 flags;
        __be16 length;
} __packed sctp_chunkhdr_t;
(include/linux/sctp.h)

以下是对sctp_chunkhdr结构成员的描述:

  • type:SCTP 型。例如,数据块的类型是 SCTP_CID_DATA。参见本章末尾“快速参考”部分的表 11-2 ,块类型,也可参见include/linux/sctp.h中的块 ID enum定义(sctp_cid_t)。
  • flags:通常情况下,发送方应将其中的 8 位全部置 0,接收方忽略。存在使用不同值的情况。例如,在 ABORT chunk 中,我们这样使用 T 位(LSB ):如果发送者填写了验证标签,则它被设置为 0,如果验证标签被反映,则它被设置为 1。
  • SCTP 块的长度。

SCTPchunk

SCTP 区块由struct sctp_chunk表示。每个块对象包含这个块的源地址和目的地址,以及一个根据其类型的子头(subh union 的成员)。例如,对于数据包,我们有sctp_datahdr子报头,对于 INIT 类型,我们有sctp_inithdr子类型:

struct sctp_chunk {
        . . .
        atomic_t refcnt;

        union {
                __u8 *v;
                struct sctp_datahdr        *data_hdr;
                struct sctp_inithdr        *init_hdr;
                struct sctp_sackhdr        *sack_hdr;
                struct sctp_heartbeathdr   *hb_hdr;
                struct sctp_sender_hb_info *hbs_hdr;
                struct sctp_shutdownhdr    *shutdown_hdr;
                struct sctp_signed_cookie  *cookie_hdr;
                struct sctp_ecnehdr        *ecne_hdr;
                struct sctp_cwrhdr         *ecn_cwr_hdr;
                struct sctp_errhdr         *err_hdr;
                struct sctp_addiphdr       *addip_hdr;
                struct sctp_fwdtsn_hdr     *fwdtsn_hdr;
                struct sctp_authhdr        *auth_hdr;
        } subh;

        struct sctp_chunkhdr    *chunk_hdr;
        struct sctphdr          *sctp_hdr;

        struct sctp_association *asoc;

        /* What endpoint received this chunk? */
        struct sctp_ep_common   *rcvr;

        . . .

        /* What is the origin IP address for this chunk?  */
        union sctp_addr source;
        /* Destination address for this chunk. */
        union sctp_addr dest;

        . . .

        /* For an inbound chunk, this tells us where it came from.
         * For an outbound chunk, it tells us where we'd like it to
         * go.  It is NULL if we have no preference.
         */
        struct sctp_transport *transport;

};
(include/net/sctp/structs.h)

我们现在将描述一个 SCTP 关联(它是 TCP 连接的对等物)。

SCTP 关联

在 SCTP,我们使用术语关联而不是关联;连接指的是两个 IP 地址之间的通信,而关联指的是可能有多个 IP 地址的两个端点之间的通信。SCTP 协会由struct sctp_association代表:

struct sctp_association {
       ...

        sctp_assoc_t assoc_id;

        /* These are those association elements needed in the cookie.  */
        struct sctp_cookie c;

        /* This is all information about our peer.  */
        struct {
                struct list_head transport_addr_list;

                . . .
                __u16 transport_count;
                __u16 port;
                . . .

                struct sctp_transport *primary_path;
                struct sctp_transport *active_path;

        } peer;

        sctp_state_t state;
        . . .
        struct sctp_priv_assoc_stats stats;
};
(include/net/sctp/structs.h).

以下是对sctp_association结构中一些重要成员的描述:

  • assoc_id:关联唯一 id。它是通过sctp_assoc_set_id()方法设置的。

  • c:附加到关联的状态 cookie ( sctp_cookie对象)。

  • peer:表示关联的对等端点的内部结构。添加对等点是通过sctp_assoc_add_peer()方法完成的;删除一个对等点是通过sctp_assoc_rm_peer()方法完成的。下面是对一些peer结构重要成员的描述:

  • transport_addr_list:表示对等体的一个或多个地址。建立关联后,我们可以使用sctp_connectx()方法向列表中添加地址或从中删除地址。

  • transport_count:对等地址列表中对等地址的计数器(transport_addr_list)。

  • primary_path:表示初始连接的地址(INIT INIT_ACK 交换)。如果主路径处于活动状态,关联将尝试始终使用主路径。

  • active_path:发送数据时当前使用的对等体的地址。

  • state:关联所在的州,如 SCTP _ 州 _ 已关闭或 SCTP _ 州 _ 已建立。本节稍后将讨论各种 SCTP 状态。

将多个本地地址添加到 SCTP 关联或从一个关联中删除多个地址可以通过sctp_bindx()系统调用来完成,以支持前面提到的多宿主特性。每个 SCTP 关联包括一个对等对象,它代表远程端点;对等对象包括远程端点的一个或多个地址的列表(transport_addr_list)。在建立关联时,我们可以通过调用sctp_connectx()系统调用向列表中添加一个或多个地址。SCTP 关联由sctp_association_new()方法创建,并由sctp_association_init()方法初始化。在任何给定时刻,SCTP 关联可以处于 8 种状态之一;因此,例如,当它被创建时,它的状态是 STATE _ 状态 _ 关闭。稍后,这些状态会改变;例如,请参阅本章后面的“设置 SCTP 协会”一节。这些状态由sctp_state_t enum ( include/net/sctp/constants.h)表示。

要在两个端点之间发送数据,必须完成初始化过程。在该过程中,设置这两个端点之间的 SCTP 关联;cookie 机制用于防止同步攻击。这一过程将在下一节中讨论。

成立 SCTP 协会

初始化过程是一个 4 次握手,包括以下步骤:

  • 一个端点(“A”)向它想要与之通信的端点(“Z”)发送 INIT 块。该块将在 INIT 块的 Initiate 标记字段中包括本地生成的标记,并且它还将包括值为 0(零)的验证标记(SCTP 报头中的vtag)。
  • 发送初始化块后,关联进入 SCTP 状态 COOKIE 等待状态。
  • 另一个端点(“Z”)向“A”发送 INIT-ACK 块作为回复。该块将包括在 INIT-ACK 块的发起标签字段中本地生成的标签和作为验证标签的远程发起标签(SCTP 报头中的vtag)。“Z”还应该生成一个状态 cookie,并将其与 INIT-ACK 回复一起发送。
  • 当“A”接收到 INIT-ACK 块时,它离开 SCTP 状态 COOKIE 等待状态。从现在开始,“A”将在所有发送的数据包中使用远程启动标签作为验证标签(SCTP 报头中的vtag)。“A”将在 cookie 回送块中发送它接收到的状态 COOKIE。“A”将进入 SCTP 状态 COOKIE 回应状态。
  • 当“Z”接收到 COOKIE ECHO 块时,它将构建一个 TCB(传输控制块)。TCB 是一种数据结构,包含 SCTP 连接两端的连接信息。“Z”将进一步将其状态改变为 STATE _ 状态 _ 已建立,并以 COOKIE ACK 组块进行回复。这是在“Z”上最终建立关联的地方,此时,该关联将使用保存的标签。
  • 当“A”接收到 COOKIE ACK 时,它将从 SCTP 状态 COOKIE 回应状态转移到 SCTP 状态已建立状态。

image 注意当一些强制参数丢失时,或者当接收到无效的参数值时,端点可以用中止块来响应 INIT、INIT ACK 或 COOKIE ECHO 块。应该在回复中指定中止块的原因。

现在您已经了解了 SCTP 关联以及它们是如何创建的,您将看到 SCTP 如何接收 SCTP 数据包,以及 SCTP 数据包是如何发送的。

用 SCTP 接收数据包

接收 SCTP 包的主要处理程序是sctp_rcv()方法,它获取一个 SKB 作为单个参数(net/sctp/input.c)。首先进行一些健全性检查(大小、校验和等)。如果一切正常,我们继续检查这个数据包是否是一个“出乎意料的”(OOTB)数据包。如果数据包的格式正确(即没有校验和错误),则该数据包是 OOTB 数据包,但是接收方无法识别该数据包所属的 SCTP 协会。(参见 RFC 4960 中的第 8.4 节。)OOTB 包由sctp_rcv_ootb()方法处理,该方法遍历包的所有块,并根据 RFC 中指定的块类型采取行动。因此,例如,放弃中止的块。如果这个包不是 OOTB 包,它通过调用sctp_inq_push()方法被放入 SCTP inqueue,并通过sctp_assoc_bh_rcv()方法或sctp_endpoint_bh_rcv()方法继续它的旅程。

用 SCTP 发送数据包

对用户空间 SCTP 套接字的写入到达sctp_sendmsg()方法(net/sctp/socket.c)。通过调用sctp_primitive_SEND()方法将数据包传递到较低层,该方法又调用状态机回调sctp_do_sm() ( net/sctp/sm_sideeffect.c),带有 SCTP_ST_PRIMITIVE_SEND。下一个阶段是调用sctp_side_effects(),最终调用sctp_packet_transmit()方法。

SCTP 心跳

心跳机制通过交换心跳和心跳确认 SCTP 数据包来测试传输或路径的连通性。一旦达到未返回心跳确认的阈值,它就声明传输 IP 地址关闭。默认情况下,每 30 秒发送一次心跳块,以监控空闲目标传输地址的可达性。该时间间隔可通过设置/proc/sys/net/sctp/hb_interval进行配置。默认值为 30000 毫秒(30 秒)。发送心跳块是由sctp_sf_sendbeat_8_3()方法执行的。方法名中出现8_3的原因是它引用了 RFC 4960 中的 8.3 节(路径心跳)。当端点接收到心跳块时,如果它处于 SCTP 状态 COOKIE 回应状态或 SCTP 状态已建立状态,它将使用心跳回应块进行回复。

SCTP 多数据流

流是单个关联中的单向数据流。出站流的数量和入站流的数量是在关联设置期间声明的(由 INIT chunk 声明),并且这些流在整个关联生存期内都是有效的。用户空间应用可以通过创建一个sctp_initmsg对象并初始化它的sinit_num_ostreamssinit_max_instreams,然后用 SCTP_INITMSG 调用setsockopt()方法来设置流的数量。流数量的初始化也可以通过sendmsg()系统调用来完成。这反过来设置了sctp_sock对象的initmsg对象中的相应字段。添加流的最大原因之一是消除行首阻塞 (HoL 阻塞)情况。行首阻塞是一种性能限制现象,当一行数据包被第一个数据包阻塞时会发生这种现象,例如,在 HTTP 管道中的多个请求中。使用 SCTP 多数据流时,不存在这个问题,因为每个数据流都是单独排序的,并保证按顺序传送。因此,一旦其中一个流由于丢失/拥塞而被阻塞,其他流可能不会被阻塞,并且数据将继续被传送。这是因为一个流可以被阻塞,而其他流没有被阻塞,

image 注意关于为 SCTP 使用套接字,我要提一下lksctp-tools项目(http://lksctp.sourceforge.net/)。这个项目为 SCTP ( libsctp)提供了一个 Linux 用户空间库,包括 C 语言头文件(netinet/sctp.h),用于访问标准套接字没有提供的特定于 SCTP 的应用编程接口,以及 SCTP 周围的一些帮助实用程序。我还应该提到 RFC 6458,“流控制传输协议(SCTP)的套接字 API 扩展”,它描述了流控制传输协议(SCTP)到套接字 API 的映射。

SCTP 多宿主

SCTP 多宿主是指在两个端点上都有多个 IP 地址。SCTP 的一个非常好的特性是,如果本地 ip 地址被指定为通配符,端点默认是多宿主的。此外,关于多宿主特性也有很多困惑,因为人们期望简单地通过绑定到多个地址,关联将最终成为多宿主的。这是不正确的,因为我们只实现目的地多宿主。换句话说,两个连接的端点都必须是多宿主的,它才能具有真正的故障转移能力。如果本地关联只知道一个目的地址,则只有一条路径,因此没有多宿主。

随着本节对 SCTP 多宿主的描述,本章的 SCTP 部分也就结束了。在下一节,我将介绍 DCCP 协议,这是本章讨论的最后一个传输协议。

DCCP:数据报拥塞控制协议

DCCP 是一种不可靠的拥塞控制传输层协议,因此它借鉴了 UDP 和 TCP 的优点,同时增加了新功能。和 UDP 一样,它是面向消息的,不可靠。像 TCP 一样,它是面向连接的协议,也使用 3 次握手来建立连接。通过几个研究机构的参与,DCCP 的开发得到了学术界想法的帮助,但是到目前为止它还没有在更大规模的互联网系统中进行测试。例如,在需要较小延迟的应用中,以及在允许少量数据丢失的应用中,如电话和流媒体应用中,使用 DCCP 是有意义的。

DCCP 中的拥塞控制与 TCP 中的不同之处在于,拥塞控制算法(称为 CCID)可以在端点之间协商,并且拥塞控制可以应用于连接(在 DCCP 称为半连接)的正向和反向路径。到目前为止,已经规定了两类可插拔拥塞控制。第一种是基于速率的、平滑的“TCP 友好”算法(CCID-3,RFC 4342 和 5348),对于这种算法,有一个实验性的小数据包变体,称为 CCID-4 (RFC 5622,RFC 4828)。第二种类型的拥塞控制,“类 TCP”(RFC 4341)将带有选择性确认的基本 TCP 拥塞控制算法(SACK,RFC 2018)应用于 DCCP 流。端点至少需要实现一个 CCID 才能运行。第一个 DCCP Linux 实现在 Linux 内核 2.6.14 (2005)中发布。本章描述了 DCCPv4 (IPv4)的实现原理。深入研究单个 DCCP 拥塞控制算法的实现细节超出了本书的范围。

现在我已经大致介绍了 DCCP 协议,接下来我将描述 DCCP 报头。

DCCP 头球

每个 DCCP 数据包都以 DCCP 报头开始。DCCP 报头的最小长度是 12 个字节。DCCP 使用可变长度的报头,长度范围从 12 到 1020 个字节,具体取决于是否使用短序列号以及使用哪些 TLV 数据包选项。DCCP 序列号针对每个数据包递增(而不是像 TCP 中那样针对每个字节),并且可以从 6 个字节缩短到 3 个字节。

struct dccp_hdr {
        __be16  dccph_sport,
                dccph_dport;
        __u8    dccph_doff;
#if defined(__LITTLE_ENDIAN_BITFIELD)
        __u8    dccph_cscov:4,
                dccph_ccval:4;
#elif defined(__BIG_ENDIAN_BITFIELD)
        __u8    dccph_ccval:4,
                dccph_cscov:4;
#else
#error  "Adjust your <asm/byteorder.h> defines"
#endif
        __sum16 dccph_checksum;
#if defined(__LITTLE_ENDIAN_BITFIELD)
        __u8    dccph_x:1,
                dccph_type:4,
                dccph_reserved:3;
#elif defined(__BIG_ENDIAN_BITFIELD)
        __u8    dccph_reserved:3,
                dccph_type:4,
                dccph_x:1;
#else
#error  "Adjust your <asm/byteorder.h> defines"
#endif
        __u8    dccph_seq2;
        __be16  dccph_seq;
};
(include/uapi/linux/dccp.h)

以下是对dccp_hdr结构中重要成员的描述:

  • dccph_sport:源端口(16 位)。
  • dccph_dport:目的端口(16 位)。
  • dccph_doff:数据偏移(8 位)。DCCP 报头的大小是 4 字节的倍数。
  • dccph_cscov:确定校验和包含数据包的哪一部分。当部分校验和用于可以容忍较低百分比的损坏的应用时,它可能会提高性能。
  • dccph_ccval:从发送者到接收者的 CCID 特定信息(不总是使用)。
  • dccph_x:扩展序列号位(1 位)。当使用 48 位扩展序列号和确认号时,设置该标志。
  • dccph_type:DCCP 头类型(4 位)。例如,这可以是用于数据分组的 DCCP 数据包数据或用于确认的 DCCP 数据包确认。参见本章末尾“快速参考”部分的表 11-3 、【DCCP 数据包类型】。
  • dccph_reserved:保留以备将来使用(1 位)。
  • dccph_checksum:校验和(16 位)。DCCP 报头和数据的 Internet 校验和,计算方法类似于 UDP 和 TCP。如果使用部分校验和,则只对应用数据的dccph_cscov指定的长度进行校验和检查。
  • dccph_seq2:顺序号。这在处理扩展序列号(8 位)时使用。
  • dccph_seq:顺序号。对于每个数据包(16 位),该值递增 1。

image DCCP 序号取决于dccph_x。(详见dccp_hdr_seq()法,include/linux/dccp.h)。

图 11-4 显示了一个 DCCP 割台。设置了dccph_x标志,所以我们使用 48 位扩展序列号。

9781430261964_Fig11-04.jpg

图 11-4 。DCCP 报头(扩展序列号位被置位,dccph_x=1)

图 11-5 显示了一个 DCCP 割台。没有设置dccph_x标志,所以我们使用 24 位序列号。

9781430261964_Fig11-05.jpg

图 11-5 。DCCP 报头(扩展序列号位未置位,dccph_x=0)

DCCP 初始化

DCCP 初始化与 TCP 和 UDP 中的情况非常相似。考虑到 DCCPv4 的情况(net/dccp/ipv4.c),首先定义一个proto对象(dccp_v4_prot,并设置其 DCCP 特定的回调;我们还定义了一个net_protocol对象(dccp_v4_protocol)并初始化它:

static struct proto dccp_v4_prot = {
         .name                   = "DCCP",
         .owner                  = THIS_MODULE,
         .close                  = dccp_close,
         .connect                = dccp_v4_connect,
         .disconnect             = dccp_disconnect,
         .ioctl                  = dccp_ioctl,
         .init                   = dccp_v4_init_sock,
         . . .
         .sendmsg                = dccp_sendmsg,
         .recvmsg                = dccp_recvmsg,
         . . .

}

(net/dccp/ipv4.c)

static const struct net_protocol dccp_v4_protocol = {
        .handler        = dccp_v4_rcv,
        .err_handler    = dccp_v4_err,
        .no_policy      = 1,
        .netns_ok       = 1,
};

(net/dccp/ipv4.c)

我们在dccp_v4_init()方法中注册了dccp_v4_prot对象和dccp_v4_protocol对象:

static int __init dccp_v4_init(void)
{
         int err = proto_register(&dccp_v4_prot, 1);

         if (err != 0)
                 goto out;

         err = inet_add_protocol(&dccp_v4_protocol, IPPROTO_DCCP);
         if (err != 0)
                 goto out_proto_unregister;
(net/dccp/ipv4.c)

DCCP 套接字初始化

从用户空间在 DCCP 创建套接字使用了socket()系统调用,其中域参数(SOCK_DCCP)表示要创建一个 DCCP 套接字。在内核中,这导致 DCCP 套接字通过dccp_v4_init_sock()回调进行初始化,这依赖于dccp_init_sock()方法来执行实际的工作:

static int dccp_v4_init_sock(struct sock *sk)
{
        static __u8 dccp_v4_ctl_sock_initialized;
        int err = dccp_init_sock(sk, dccp_v4_ctl_sock_initialized);

        if (err == 0) {
                if (unlikely(!dccp_v4_ctl_sock_initialized))
                        dccp_v4_ctl_sock_initialized = 1;
                inet_csk(sk)->icsk_af_ops = &dccp_ipv4_af_ops;
        }

        return err;
}
(net/dccp/ipv4.c)

dccp_init_sock()方法最重要的任务如下:

  • 用相同的默认值初始化 DCCP 套接字字段(例如,套接字状态设置为 DCCP _ 关闭)
  • DCCP 定时器的初始化(通过dccp_init_xmit_timers()方法)
  • 通过调用dccp_feat_init()方法初始化特征协商部分。功能协商是 DCCP 的一个显著特征,通过它,端点可以就连接每一端的属性达成一致。它扩展了 TCP 功能协商,并在 RFC 4340,sec。6.

使用 DCCP 从网络层(L3)接收数据包

从网络层(L3)接收 DCCP 数据包的主要处理程序是dccp_v4_rcv ()方法:

static int dccp_v4_rcv(struct sk_buff *skb)
{
        const struct dccp_hdr *dh;
        const struct iphdr *iph;
        struct sock *sk;
        int min_cov;

首先,我们丢弃无效的数据包。例如,如果数据包不是针对该主机的(数据包类型不是 PACKET_HOST),或者数据包的大小比 DCCP 报头(12 字节)短:

        if (dccp_invalid_packet(skb))
                  goto discard_it;

然后,我们根据流程执行查找:

        sk = __inet_lookup_skb(&dccp_hashinfo, skb,
                               dh->dccph_sport, dh->dccph_dport);

如果找不到套接字,数据包将被丢弃:

        if (sk == NULL) {
               . . .
               goto no_dccp_socket;
        }

我们做了一些与最小校验和覆盖相关的检查,如果一切正常,我们继续使用通用的sk_receive_skb()方法将数据包传递到传输层(L4)。注意dccp_v4_rcv()方法在结构和功能上与tcp_v4_rcv()方法非常相似。这是因为 Linux 中的 DCCP 的原作者阿纳尔多·卡瓦略·德·梅洛已经非常努力地在代码中使 TCP 和 DCCP 之间的相似之处变得明显而清晰。

         . . .
         return sk_receive_skb(sk, skb, 1);
         }
(net/dccp/ipv4.c)

用 DCCP 发送数据包

从 DCCP 用户空间套接字发送数据最终由内核中的dccp_sendmsg()方法处理(net/dccp/proto.c)。这类似于 TCP 的情况,其中tcp_sendmsg()内核方法处理从 TCP 用户空间套接字发送的数据。我们来看看dccp_sendmsg()的方法:

int dccp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
                  size_t len)
{
         const struct dccp_sock *dp = dccp_sk(sk);
         const int flags = msg->msg_flags;
        const int noblock = flags & MSG_DONTWAIT;
        struct sk_buff *skb;
        int rc, size;
        long timeo;

分配一个 SKB:

        skb = sock_alloc_send_skb(sk, size, noblock, &rc);
        lock_sock(sk);
        if (skb == NULL)
                goto out_release;

        skb_reserve(skb, sk->sk_prot->max_header);

将数据块从msghdr对象复制到 SKB:

        rc = memcpy_fromiovec(skb_put(skb, len), msg->msg_iov, len);
        if (rc != 0)
                goto out_discard;

        if (!timer_pending(&dp->dccps_xmit_timer))
                dccp_write_xmit(sk);

根据为连接选择的拥塞控制类型(基于窗口或基于速率),dccp_write_xmit()方法将使数据包稍后发送(通过dccps_xmit_timer()到期)或通过dccp_xmit_packet()方法立即发送。这又依赖于dccp_transmit_skb()方法来初始化传出的 DCCP 报头,并将其传递给 L3 特定的queue_xmit发送回调(对 IPv4 使用ip_queue_xmit()方法,对 IPv6 使用inet6_csk_xmit()方法)。我将用一小段关于 DCCP 和纳特的话来结束我们关于 DCCP 的讨论。

DCCP 和纳特

一些 NAT 设备不允许 DCCP 通过(通常是因为它们的固件通常很小,因此不支持“外来的”IP 协议,如 DCCP)。RFC 5597(2009 年 9 月)提出了 NAT 的行为要求,以支持 NAT-ed DCCP 通信。然而,目前还不清楚这些建议在多大程度上被应用到消费者设备中。DCCP-UDP 的动机之一是缺少让 DCCP 通过的 NAT 设备。1).在与 TCP 的比较中,有一个细节可能很有趣。默认情况下,后者支持同时打开(RFC 793,第 3.4 节),而 RFC 4340 第 4.6 节中 DCCP 的初始规范不允许使用同时打开。为了支持 NAPT 遍历,RFC 5596 在 2009 年 9 月更新了 RFC 4340,采用了“近乎同时打开”技术,在列表中添加了一种数据包类型(DCCP 监听,RFC 5596,第 2.2.1 节),并更改了状态机以支持另外两种状态(2.2.2 ),从而支持近乎同时打开。动机是 NAT“打孔”技术,然而,这需要存在 DCCP 的 NAT(问题同上)。由于这个先有鸡还是先有蛋的问题,DCCP 在互联网上没有看到太多的曝光。也许 UDP 封装会改变这一点。但这样一来,它就不再被视为真正的传输层协议。

摘要

本章讨论了四种传输协议:最常用的 UDP 和 TCP,以及较新的协议 SCTP 和 DCCP。您了解了这些协议之间的基本区别。您了解了 TCP 是一种比 UDP 复杂得多的协议,因为它使用一个状态机和几个定时器,并且需要确认。您了解了每种协议的报头,以及如何使用这些协议发送和接收数据包。我讨论了 SCTP 协议的一些独特功能,比如多宿主和多数据流。

下一章将讨论无线子系统及其在 Linux 中的实现。在接下来的“快速参考”部分,我将介绍与本章中讨论的主题相关的顶级方法,按照它们的上下文进行排序,并且我还将展示本章中提到的两个表。

快速参考

我将用本章中讨论的套接字和传输层协议的重要方法的简短列表来结束本章。本章提到了其中一些。之后,有一个宏和三个表。

方法

下面是方法。

int ip_cmsg_send(struct net *net,struct msghdr *msg,struct ipcm _ cookie * IPC);

该方法通过解析指定的msghdr对象构建一个ipcm_cookie对象。

void sock _ put(struct sock * sk);

该方法减少指定的sock对象的引用计数。

void sock _ hold(struct sock * sk);

该方法增加指定sock对象的引用计数。

int sock_create(int family,int type,int protocol,struct socket * * RES);

这个方法执行一些健全性检查,如果一切正常,它通过调用sock_alloc()方法分配一个套接字,然后调用net_families[family]->create。(在 IPv4 的情况下,它是inet_create()方法。)

int sock _ map _ FD(struct socket * sock,int flags);

这个方法分配一个文件描述符并填充文件条目。

bool sock _ flag(const struct sock * sk,enum sock_flags 标志);

如果在指定的sock对象中设置了指定的flag,则该方法返回true

内部 TCP _ v4 _ rcv(struct sk _ buf * skb):

此方法是处理来自网络层(L3)的传入 TCP 数据包的主要处理程序。

void TCP _ init _ sock(struct sock * sk);

此方法执行独立于地址族的套接字初始化。

struct tcphdr * TCP _ HDR(const struct sk _ buff * skb);

该方法返回与指定的skb相关联的 TCP 报头。

int TCP _ send msg(struct ki OCB * iocb,struct sock *sk,struct msghdr *msg,size _ t size);

这个方法处理从用户空间发送的 TCP 数据包。

struct TCP _ sock * TCP _ sk(const struct sock * sk);

该方法返回与指定 sock 对象(sk)关联的tcp_sock对象。

int UDP _ rcv(struct sk _ buf * skb);

此方法是处理来自网络层(L3)的 UDP 数据包的主要处理程序。

struct UDP HDR * UDP _ HDR(const struct sk _ buff * skb);

该方法返回与指定的skb相关联的 UDP 头。

int UDP _ sendmail(struct KIO CB * iocb、struct sock *sk、struct msghdr * msg、size _ t len);

这个方法处理从用户空间发送的 UDP 包。

sctphdr *sctp_hdr 结构(const struct sk _ buf * skb);

该方法返回与指定的skb相关联的 SCTP 头。

struct SCTP _ sock * SCTP _ sk(const struct sock * sk):

该方法返回与指定的sock对象相关联的 SCTP 套接字(sctp_sock对象)。

int SCTP _ sendmail(struct KIO CB * iocb、struct sock *sk、struct msghdr * msg、size _ t msg _ len);

该方法处理从用户空间发送的 SCTP 数据包。

struct SCTP _ association * SCTP _ association _ new(const struct SCTP _ endpoint * EP,const struct sock *sk,sctp_scope_t scope,GFP _ t GFP);

这个方法分配并初始化一个新的 SCTP 关联。

void SCTP _ association _ free(struct SCTP _ association * asoc);

这种方法释放了 SCTP 协会的资源。

void SCTP _ chunk _ hold(struct SCTP _ chunk * ch);

此方法递增指定 SCTP 块的引用计数。

void SCTP _ chunk _ put(struct SCTP _ chunk * ch);

此方法递减指定 SCTP 块的引用计数。如果引用计数达到 0,它通过调用sctp_chunk_destroy()方法来释放它。

int SCTP _ rcv(struct sk _ buf * skb):

这个方法是输入 SCTP 包的主要输入处理程序。

static int dccp _ v4 _ rcv(struct sk _ buff * skb);

此方法是处理来自网络层(L3)的 DCCP 数据包的主要 Rx 处理程序。

int dccp _ sendmail(struct KIO CB * iocb、struct sock *sk、struct msghdr * msg、size _ t len);

这个方法处理从用户空间发送的 DCCP 包。

宏指令

这是宏指令。

sctp_chunk_is_data()

如果指定的块是数据块,此宏返回 1;否则,它返回 0。

桌子

看看本章中使用的表格。

表 11-1。 TCP 和 UDP prot_ops 对象

|

prot_ops 回调

|

三氯苯酚

|

用户数据报协议(User Datagram Protocol)

|
| — | — | — |
| release | inet_release | inet_release |
| bind | inet_bind | inet_bind |
| connect | inet_stream_connect | inet_dgram_connect |
| socketpair | sock_no_socketpair | sock_no_socketpair |
| accept | inet_accept | sock_no_accept |
| getname | inet_getname | inet_getname |
| poll | tcp_poll | udp_poll |
| ioctl | inet_ioctl | inet_ioctl |
| listen | inet_listen | sock_no_listen |
| shutdown | inet_shutdown | inet_shutdown |
| setsockopt | sock_common_setsockopt | sock_common_setsockopt |
| getsockopt | sock_common_getsockopt | sock_common_getsockopt |
| sendmsg | inet_sendmsg | inet_sendmsg |
| recvmsg | inet_recvmsg | inet_recvmsg |
| mmap | sock_no_mmap | sock_no_mmap |
| sendpage | inet_sendpage | inet_sendpage |
| splice_read | tcp_splice_read | - |
| compat_setsockopt | compat_sock_common_setsockopt | compat_sock_common_setsockopt |
| compat_getsockopt | compat_sock_common_getsockopt | compat_sock_common_getsockopt |
| compat_ioctl | inet_compat_ioctl | inet_compat_ioctl |

image 参见net/ipv4/af_inet.cinet_stream_opsinet_dgram_ops的定义。

表 11-2 。组块类型

|

组块类型

|

Linux 符号

|

价值

|
| — | — | — |
| 有效载荷数据 | SCTP_CID_DATA | Zero |
| 开始 | SCTP_CID_INIT | one |
| 初始确认 | SCTP_CID_INIT_ACK | Two |
| 选择性确认 | SCTP_CID_SACK | three |
| 心跳请求 | SCTP 心跳 | four |
| 心跳确认 | SCTP 心跳确认 | five |
| 流产 | SCTP_CID_ABORT | six |
| 关机 | SCTP_CID_SHUTDOWN | seven |
| 关闭确认 | SCTP_CID_SHUTDOWN_ACK | eight |
| 操作误差 | SCTP_CID_ERROR | nine |
| 状态 Cookie | SCTP_CID_COOKIE_ECHO | Ten |
| Cookie 确认 | SCTP_CID_COOKIE_ACK | Eleven |
| 显式拥塞通知回应(ECNE) | SCTP _ cid _ ECN _ exception | Twelve |
| 拥塞窗口减少(CWR) | SCTP_CID_ECN_CWR | Thirteen |
| 关闭完成 | SCTP _ CID _ 关闭 _ 完成 | Fourteen |
| SCTP 身份验证块(RFC 4895) | SCTP_CID_AUTH | 0x0F |
| 传输序列号 | SCTP_CID_FWD_TSN | 0xC0 |
| 地址配置更改块 | sctp _ cid _ asconf | 0xC1 |
| 地址配置确认块 | sctp _ cid _ asconf _ ack | 0x80 |

表 11-3 。DCCP 数据包类型

|

Linux 符号

|

描述

|
| — | — |
| DCCP _ PKT _ 请求 | 由客户端发送以启动连接(三次启动握手的第一部分)。 |
| DCCP _ PKT _ 回应 | 由服务器发送以响应 DCCP 请求(三次启动握手的第二部分)。 |
| dccp _ pkt _ 数据 | 用于传输应用数据。 |
| DCCP_PKT_ACK | 用于传输纯确认。 |
| DCCP_PKT_DATAACK | 用于传输带有附带确认信息的应用数据。 |
| DCCP_PKT_CLOSEREQ | 由服务器发送,请求客户端关闭连接。 |
| DCCP_PKT_CLOSE | 由客户端或服务器用来关闭连接;引发一个 DCCP 重置包作为响应。 |
| DCCP_PKT_RESET | 用于正常或异常终止连接。 |
| DCCP_PKT_SYNC | 用于在数据包大量丢失后重新同步序列号。 |
| DCCP_PKT_SYNCACK | 确认 DCCP_PKT_SYNC。 |

Logo

获取更多汽车电子技术干货

更多推荐