============================
网络
============================

.. meta::
   :description: 介绍 Linux 内核网络子系统的架构和编程,包括网络编程在用户空间和内核空间的实现,以及网络数据包的处理机制
   :keywords: Linux, 内核, 网络编程, 套接字, TCP/IP, 数据包, sk_buff, socket

实验目标
==============

  * 理解 Linux 内核网络架构
  * 掌握使用数据包(packet)过滤器或防火墙进行 IP 数据包管理
  * 熟悉在 Linux 内核级别使用套接字的方法

概述
========

互联网的发展导致网络应用程序呈指数增长,因此操作系统的网络子系统对速度和生产力的要求也在不断增加。网络子系统并非操作系统内核的必需组件(Linux 内核可以选择在编译时不包含网络支持)。然而,由于对连接的需要,计算机系统(甚至嵌入式设备)很少会使用不支持网络的操作系统。现代操作系统使用 `TCP/IP 协议栈 <https://zh.wikipedia.org/zh-cn/TCP/IP协议族>`_。它们的内核实现了传输层以下的协议,而应用层协议通常在用户空间实现(如 HTTP、FTP 以及 SSH 等)。

用户空间中的网络编程
------------------

套接字(socket)是在用户空间中,对于网络通信的抽象。套接字抽象了通信通道,也是基于内核的 TCP/IP 栈交互接口。IP 套接字与 IP 地址、所使用的传输层协议(如 TCP、UDP 等)和端口相关联。常用的使用套接字的函数调用有:创建 (``socket``)、初始化 (``bind``)、连接 (``connect``)、等待连接 (``listen``, ``accept``) 以及关闭套接字 (``close``)。

我们通过 ``read``/``write`` 或 ``recv``/``send`` 调用实现 TCP 套接字的网络通信,通过 ``recvfrom``/``sendto`` 调用实现 UDP 套接字的网络通信。传输和接收操作对应用程序来说是透明的,封装和网络传输由内核自行决定。然而,也可以使用原始套接字(创建套接字时使用 ``PF_PACKET`` 选项)在用户空间中实现 TCP/IP 栈,或者在内核中实现应用层协议(例如 `TUX web 服务器 <https://zh.wikipedia.org/zh-cn/TUX_Web服务器>`_)。

有关使用套接字进行用户空间编程的更多详细信息,请参阅 `Beej 的网络套接字编程指南 <https://www.beej.us/guide/bgnet/>`_。

Linux 网络编程
================

Linux 内核提供了三种基本的用于处理网络数据包的结构::c:type:`struct socket`、:c:type:`struct sock` 和 :c:type:`struct sk_buff`。

前两者是对套接字的抽象:

  * :c:type:`struct socket` 是非常接近用户空间的抽象,即用于编写网络应用程序的 `BSD 套接字 <http://zh.wikipedia.org/zh-cn/Berkeley套接字>`_;
  * :c:type:`struct sock` 或 Linux 术语中的 *INET 套接字* 是套接字的网络表示。

这两个结构有关联: :c:type:`struct socket` 包含 INET 套接字字段,而每个 :c:type:`struct sock` 都有一个 BSD 套接字持有它。

:c:type:`struct sk_buff` 结构是网络数据包及其状态的表示。当从用户空间或网络接口接收到内核数据包时,该结构被创建。

:c:type:`struct socket` 结构
-------------------------------------

:c:type:`struct socket` 结构是 BSD 套接字的内核表示,可以执行在它上面执行的操作类似于内核提供的操作(通过系统调用)。使用套接字的常见操作(创建、初始化/绑定、关闭等)会导致特定的系统调用;它们与 :c:type:`struct socket` 结构一起工作。

:c:type:`struct socket` 的操作在 :file:`net/socket.c` 中进行描述,且与协议类型无关。因此, :c:type:`struct socket` 结构是特定网络操作实现的通用接口。通常,这些操作的名称以 ``sock_`` 前缀开头。

.. _SocketStructOps:

对 socket 结构的操作
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

socket 相关操作包括:

创建
""""""""

创建类似于在用户空间调用 :c:func:`socket` 函数,但创建的 :c:type:`struct socket` 将存储在 ``res`` 参数中:

  * ``int sock_create(int family, int type, int protocol, struct socket **res)``:在 :c:func:`socket` 系统调用之后创建 socket;
  * ``int sock_create_kern(struct net *net, int family, int type, int protocol, struct socket **res)``:创建内核 socket;
  * ``int sock_create_lite(int family, int type, int protocol, struct socket **res)``:创建内核 socket, 不经过参数完整性检查。

这些调用的参数如下:

  * ``net`` (如果存在) 用作对所使用的网络命名空间的引用;通常我们会使用 ``init_net`` 进行初始化;
  * ``family`` 表示在信息传输中使用的协议族;它们通常以 ``PF_`` (协议族) 字符串开头;表示所使用的协议族的常量可以在 :file:`linux/socket.h` 中找到,其中最常用的是 ``PF_INET``, 用于 TCP/IP 协议;
  * ``type`` 是 socket 的类型;用于此参数的常量可以在 :file:`linux/net.h` 中找到,其中最常用的是 ``SOCK_STREAM`` (用于基于连接的源到目的地通信) 以及 ``SOCK_DGRAM`` (用于无连接通信);
  * ``protocol`` 表示使用的协议,与 ``type`` 参数密切相关;用于此参数的常量可以在 :file:`linux/in.h` 中找到,其中最常用的是 ``IPPROTO_TCP`` (用于 TCP), ``IPPROTO_UDP`` (用于 UDP)。

要在内核空间中创建 TCP socket,你需要调用:

.. code-block:: c

  	struct socket *sock;
  	int err;

  	err = sock_create_kern(&init_net, PF_INET, SOCK_STREAM, IPPROTO_TCP, &sock);
  	if (err < 0) {
  		/* 处理错误 */
  	}

要在内核空间中创建 UDP socket,你需要调用:

.. code-block:: c

  	struct socket *sock;
  	int err;

  	err = sock_create_kern(&init_net, PF_INET, SOCK_DGRAM, IPPROTO_UDP, &sock);
  	if (err < 0) {
  		/* 处理错误 */
  	}

一个使用示例是 :c:func:`sys_socket` 系统调用处理程序的一部分:

.. code-block:: c

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

  	/* 检查 SOCK_* 常量是否一致。 */
  	BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
  	BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
  	BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
  	BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);

  	flags = type & ~SOCK_TYPE_MASK;
  	if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
  		return -EINVAL;
  	type &= SOCK_TYPE_MASK;

  	if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
  		flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

  	retval = sock_create(family, type, protocol, &sock);
  	if (retval < 0)
  		goto out;

  	return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
  }

关闭连接
"""""""

关闭连接(对于使用连接的 socket)并释放相关资源:

  * ``void sock_release(struct socket *sock)`` 调用 socket 结构 ``ops`` 字段中的 ``release`` 函数:

.. code-block:: c

  void sock_release(struct socket *sock)
  {
  	if (sock->ops) {
  		struct module *owner = sock->ops->owner;

  		sock->ops->release(sock);
  		sock->ops = NULL;
  		module_put(owner);
  	}
  	//...
  }

发送/接收消息
""""""""""""""""""""""""""

使用以下函数来发送/接收消息:

  * ``int sock_recvmsg(struct socket *sock, struct msghdr *msg, int flags);``
  * ``int kernel_recvmsg(struct socket *sock, struct msghdr *msg, struct kvec *vec, size_t num, size_t size, int flags);``
  * ``int sock_sendmsg(struct socket *sock, struct msghdr *msg);``
  * ``int kernel_sendmsg(struct socket *sock, struct msghdr *msg, struct kvec *vec, size_t num, size_t size);``

消息的发送/接收函数将调用 socket ``ops`` 字段中的 ``sendmsg``/``recvmsg`` 函数。当 socket 在内核中使用时,应使用以 ``kernel_`` 为前缀的函数。

参数包括:

  * ``msg``, :c:type:`struct msghdr` 结构,包含要发送/接收的消息。该结构的重要组成部分包括 ``msg_name`` 和 ``msg_namelen``,对于 UDP 套接字,必须使用目标地址填充 (:c:type:`struct sockaddr_in`);
  * ``vec``, :c:type:`struct kvec` 结构,其中有一个指针指向缓冲区,缓冲区内包含该 :c:type:`struct kvec` 结构的数据和大小;正如所见,它的结构类似于 :c:type:`struct iovec` 结构 (:c:type:`struct iovec` 结构对应用户空间数据,而 :c:type:`struct kvec` 结构对应内核空间数据)。

可以在 :c:func:`sys_sendto` 系统调用处理程序中看到用法示例:

.. code-block:: c

  SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
  		unsigned int, flags, struct sockaddr __user *, addr,
  		int, addr_len)
  {
  	struct socket *sock;
  	struct sockaddr_storage address;
  	int err;
  	struct msghdr msg;
  	struct iovec iov;
  	int fput_needed;

  	err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter);
  	if (unlikely(err))
  		return err;
  	sock = sockfd_lookup_light(fd, &err, &fput_needed);
  	if (!sock)
  		goto out;

  	msg.msg_name = NULL;
  	msg.msg_control = NULL;
  	msg.msg_controllen = 0;
  	msg.msg_namelen = 0;
  	if (addr) {
  		err = move_addr_to_kernel(addr, addr_len, &address);
  		if (err < 0)
  			goto out_put;
  		msg.msg_name = (struct sockaddr *)&address;
  		msg.msg_namelen = addr_len;
  	}
  	if (sock->file->f_flags & O_NONBLOCK)
  		flags |= MSG_DONTWAIT;
  	msg.msg_flags = flags;
  	err = sock_sendmsg(sock, &msg);

  out_put:
  	fput_light(sock->file, fput_needed);
  out:
  	return err;
  }

:c:type:`struct socket` 字段
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: c

  /**
   *  struct socket——通用的 BSD socket 
   *  @state: socket 状态(%SS_CONNECTED 等)
   *  @type: socket 类型(%SOCK_STREAM 等)
   *  @flags: socket 标志(%SOCK_NOSPACE 等)
   *  @ops: 协议特定的 socket 操作
   *  @file: 反向指向 file 的指针,用于垃圾回收
   *  @sk: 内部的网络协议无关 socket 表示
   *  @wq: 有多种用途的等待队列
   */
  struct socket {
  	socket_state		state;

  	short			type;

  	unsigned long		flags;

  	struct socket_wq __rcu	*wq;

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

值得注意的字段包括:

  * ``ops``——该结构内部有指针,指针指向协议特定函数;
  * ``sk``——与之关联的 ``INET socket``。

:c:type:`struct proto_ops` 结构
""""""""""""""""""""""""""""""""""""""""

:c:type:`struct proto_ops` 结构体包含了特定操作(TCP、UDP 等)的实现;这些函数将通过通用函数(如 :c:func:`sock_release`, :c:func:`sock_sendmsg` 等)使用 :c:type:`struct socket` 为参数来调用。

因此, :c:type:`struct proto_ops` 结构体包含了一些特定协议实现的函数指针:

.. code-block:: c

  struct proto_ops {
  	int		family;
  	struct module	*owner;
  	int		(*release)   (struct socket *sock);
  	int		(*bind)      (struct socket *sock,
  				      struct sockaddr *myaddr,
  				      int sockaddr_len);
  	int		(*connect)   (struct socket *sock,
  				      struct sockaddr *vaddr,
  				      int sockaddr_len, int flags);
  	int		(*socketpair)(struct socket *sock1,
  				      struct socket *sock2);
  	int		(*accept)    (struct socket *sock,
  				      struct socket *newsock, int flags, bool kern);
  	int		(*getname)   (struct socket *sock,
  				      struct sockaddr *addr,
  				      int peer);
  	//...
  }

:c:type:`struct socket` 结构体的 ``ops`` 字段的初始化是在 :c:func:`__sock_create` 函数中完成的,该函数通过调用针对每个协议的 :c:func:`create` 函数来实现;等效调用是 :c:func:`__sock_create` 函数的实现:

.. code-block:: c

  //...
  	err = pf->create(net, sock, protocol, kern);
  	if (err < 0)
  		goto out_module_put;
  //...

这将使用与 socket 关联的协议类型特定的调用来实例化函数指针。 :c:func:`sock_register` 和 :c:func:`sock_unregister` 调用用于填充 ``net_families`` 向量。

对于 socket 结构的其余操作(除了在 `对 socket 结构的操作`_ 部分中描述的创建、关闭和发送/接收消息之外),将调用通过这个结构中的指针传递的函数。例如,对于 ``bind`` 操作(它将一个 socket 与一个本地机器上的 socket 关联)有以下代码:

.. code-block:: c

  #define MY_PORT 60000

  struct sockaddr_in addr = {
  	.sin_family = AF_INET,
  	.sin_port = htons (MY_PORT),
  	.sin_addr = { htonl (INADDR_LOOPBACK) }
  };

  //...
  	err = sock->ops->bind (sock, (struct sockaddr *) &addr, sizeof(addr));
  	if (err < 0) {
  		  /* 处理错误 */
  	}
  //...

在以上代码中,用于传输与 socket 关联的地址和端口信息的是 :c:type:`struct sockaddr_in` 结构体。

:c:type:`struct sock` 结构
-----------------------------

:c:type:`struct sock` 结构描述了 ``INET`` 套接字。这样的结构与用户空间的 socket 相关联,并且与 :c:type:`struct socket` 结构相关联,其中与 :c:type:`struct socket` 结构的关联是隐式的。该结构用于存储关于连接状态的信息。结构体的字段和相关操作通常以 ``sk_`` 字符串开头。以下列出了一些字段:

.. code-block:: c

  struct sock {
  	//...
  	unsigned int		sk_padding : 1,
  				sk_no_check_tx : 1,
  				sk_no_check_rx : 1,
  				sk_userlocks : 4,
  				sk_protocol  : 8,
  				sk_type      : 16;
  	//...
  	struct socket		*sk_socket;
  	//...
  	struct sk_buff		*sk_send_head;
  	//...
  	void			(*sk_state_change)(struct sock *sk);
  	void			(*sk_data_ready)(struct sock *sk);
  	void			(*sk_write_space)(struct sock *sk);
  	void			(*sk_error_report)(struct sock *sk);
  	int			(*sk_backlog_rcv)(struct sock *sk,
  						  struct sk_buff *skb);
  	void                    (*sk_destruct)(struct sock *sk);
  };

\

  * ``sk_protocol`` 是套接字使用的协议类型;
  * ``sk_type`` 是套接字类型 (``SOCK_STREAM``, ``SOCK_DGRAM`` 等);
  * ``sk_socket`` 是持有该套接字的 BSD 套接字;
  * ``sk_send_head`` 是用于传输的 :c:type:`struct sk_buff` 结构列表;
  * 最后的函数指针是用于不同情况的回调函数。

使用从 ``net_families`` 创建的回调函数(称为 :c:func:`__sock_create`) 来初始化 :c:type:`struct sock` 并将其附加到 BSD 套接字。以下是在 :c:func:`inet_create` 函数中初始化 IP 协议的 :c:type:`struct sock` 结构体的方法:

.. code-block:: c

  /*
   *   创建 inet 套接字。
   */

  static int inet_create(struct net *net, struct socket *sock, int protocol,
  		       int kern)
  {

  	struct sock *sk;

  	//...
  	err = -ENOBUFS;
  	sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
  	if (!sk)
  		goto out;

  	err = 0;
  	if (INET_PROTOSW_REUSE & answer_flags)
  		sk->sk_reuse = SK_CAN_REUSE;


  	//...
  	sock_init_data(sock, sk);

  	sk->sk_destruct	   = inet_sock_destruct;
  	sk->sk_protocol	   = protocol;
  	sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;
  	//...
  }

.. _StructSKBuff:

:c:type:`struct sk_buff` 结构体
--------------------------------

:c:type:`struct sk_buff` (套接字缓冲区)描述了一个网络数据包。该结构的字段包含有关报头和数据包内容、使用的协议、使用的网络设备以及指向其他 :c:type:`struct sk_buff` 的指针的信息。下面是该结构体的内容的概要描述:

.. code-block:: c

  struct sk_buff {
    union {
      struct {
        /* 这两个成员必须放在最前面。 */
        struct sk_buff *next;
        struct sk_buff *prev;

        union {
          struct net_device *dev;
          /* 一些协议可能会使用此空间来存储信息,此种情形下设备指针为 NULL。
           * UDP 接收路径就是其中之一。
           */
          unsigned long dev_scratch;
        };
      };

      struct rb_node rbnode; /* 在 netem 和 tcp 栈中使用 */
    };
    struct sock *sk;

    union {
      ktime_t tstamp;
      u64 skb_mstamp;
    };

    /*
     * 这是控制缓冲区。每一层都可以自由使用它。
     * 请将你的私有变量放在这里。如果要在多个层之间保留这些变量,首先必须进行 skb_clone()。
     * 此缓冲区由当前此 skb 的队列的所有者拥有。
     */
    char cb[48] __aligned(8);

    unsigned long _skb_refdst;
    void (*destructor)(struct sk_buff *skb);
    union {
      struct {
        unsigned long _skb_refdst;
        void (*destructor)(struct sk_buff *skb);
      };
      struct list_head tcp_tsorted_anchor;
    };
    /* ... */

    unsigned int len,
                 data_len;
    __u16 mac_len,
          hdr_len;

    /* ... */

    __be16 protocol;
    __u16 transport_header;
    __u16 network_header;
    __u16 mac_header;

    /* 私有:*/
    __u32 headers_end[0];
    /* 公有:*/

    /* 这些元素必须放在最后。有关详细信息,请参阅 alloc_skb()。*/
    sk_buff_data_t tail;
    sk_buff_data_t end;
    unsigned char *head,
                  *data;
    unsigned int truesize;
    refcount_t users;
  };

其中:

  * ``next`` 和 ``prev`` 是指向缓冲区列表中下一个和前一个元素的指针;
  * ``dev`` 是发送或接收缓冲区内容的设备;
  * ``sk`` 是与缓冲区相关联的套接字;
  * ``destructor`` 是负责释放缓冲区的回调函数;
  * ``transport_header``, ``network_header`` 和 ``mac_header`` 是数据包起始位置和各个头部起始位置之间的偏移量。它们由数据包经过的各个处理层内部维护。要获取指向头部的指针,请使用以下函数之一: :c:func:`tcp_hdr`, :c:func:`udp_hdr` 以及 :c:func:`ip_hdr` 等。原则上,每个协议都对应一个函数。这些函数用于在接收到的数据包中,获取对该协议的头部的引用。请注意,当数据包到达网络层时, ``network_header`` 字段才被设置;而当数据包到达传输层时, ``transport_header`` 字段才被设置。

`IP 头部 <https://zh.wikipedia.org/zh-cn/IPv4#首部>`_ 的结构 (:c:type:`struct iphdr`) 包含以下字段:

.. code-block:: c

  struct iphdr {
  #if defined(__LITTLE_ENDIAN_BITFIELD)
  	__u8	ihl:4,
  		version:4;
  #elif defined (__BIG_ENDIAN_BITFIELD)
  	__u8	version:4,
    		ihl:4;
  #else
  #error	"Please fix <asm/byteorder.h>"
  #endif
  	__u8	tos;
  	__be16	tot_len;
  	__be16	id;
  	__be16	frag_off;
  	__u8	ttl;
  	__u8	protocol;
  	__sum16	check;
  	__be32	saddr;
  	__be32	daddr;
  	/* 可选项由此开始 */
  };

其中:

  * ``protocol`` 是使用的传输层协议;
  * ``saddr`` 是源 IP 地址;
  * ``daddr`` 是目标 IP 地址。

`TCP 头部 <https://zh.wikipedia.org/zh-cn/传输控制协议#封包結構>`_ 的结构 (:c:type:`struct tcphdr`) 具有以下字段:

.. code-block:: c

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

其中:

  * ``source`` 是源端口;
  * ``dest`` 是目标端口;
  * 常使用的 TCP 标志包括 ``syn``, ``ack`` 以及 ``fin``;要想对其有更详细地了解,请参见此 `图表 <http://www.eventhelix.com/Realtimemantra/Networking/Tcp.pdf>`_。

`UDP 头部 <https://zh.wikipedia.org/zh-cn/用户数据报协议#UDP的分组结构>`_ 的结构(:c:type:`struct udphdr`)具有以下字段:

.. code-block:: c

  struct udphdr {
  	__be16	source;
  	__be16	dest;
  	__be16	len;
  	__sum16	check;
  };

其中:

  * ``source`` 是源端口;
  * ``dest`` 是目标端口。

访问网络数据包头部中的信息的示例如下:

.. code-block:: c

  	struct sk_buff *skb;

  	struct iphdr *iph = ip_hdr(skb);                 /* IP 头部 */
  	/* iph->saddr  - 源 IP 地址 */
  	/* iph->daddr  - 目标 IP 地址 */
  	if (iph->protocol == IPPROTO_TCP) {              /* TCP 协议 */
  		struct tcphdr *tcph = tcp_hdr(skb);      /* TCP 头部 */
  		/* tcph->source —— 源 TCP 端口 */
  		/* tcph->dest   —— 目标 TCP 端口 */
  	} else if (iph->protocol == IPPROTO_UDP) {       /* UDP 协议 */
  		struct udphdr *udph = udp_hdr(skb);      /* UDP 头部 */
  		/* udph->source —— 源 UDP 端口 */
  		/* udph->dest   —— 目标 UDP 端口 */
  	}

.. _Conversions:

转换
===========

不同系统的字,有多种字节的顺序方式 (`字节序 <http://zh.wikipedia.org/zh-cn/字节序>`_),包括: `大端序 <http://zh.wikipedia.org/zh-cn/字节序#大端序>`_ (最高位字节在前) 和 `小端序 <http://zh.wikipedia.org/zh-cn/字节序#小端序>`_ (最低位字节在前)。由于网络连接了具有不同平台的系统,因此互联网对于数值数据的存储已经强加了一种标准序列,称为 `网络字节序 <http://zh.wikipedia.org/zh-cn/字节序#网络序>`_。相反,主机计算机上,表示数值数据的字节序列称为主机字节序。从网络接收/发送的数据采用网络字节序格式,并且应该在该格式和主机字节序之间进行转换。

为了进行转换,我们使用以下宏:

  * ``u16 htons(u16 x)`` 将 16 位整数从主机字节序转换为网络字节序(主机到网络短整数);
  * ``u32 htonl(u32 x)`` 将 32 位整数从主机字节序转换为网络字节序(主机到网络长整数);
  * ``u16 ntohs(u16 x)`` 将 16 位整数从网络字节序转换为主机字节序(网络到主机短整数);
  * ``u32 ntohl(u32 x)`` 将 32 位整数从网络字节序转换为主机字节序(网络到主机长整数)。

.. _netfilter:

netfilter
=========

Netfilter 是一个内核接口,用于捕获网络数据包以对其进行修改/分析(用于过滤、NAT 等)。在用户空间中,由 `iptables <http://www.frozentux.net/documents/iptables-tutorial/>`_ 使用 `netfilter <http://www.netfilter.org/>`_ 接口。

在 Linux 内核中,使用 netfilter 进行数据包捕获是通过附加钩子(hook)来实现的。钩子可以在内核网络数据包所经过的路径的不同位置指定,你可以根据需要进行配置。你可以在 `这里 <http://linux-ip.net/nf/nfk-traversal.png>`_ 找到一张组织图,组织图上显示数据包所经过的路径以及钩子可能出现的区域。

使用 netfilter 时包含的头文件是 :file:`linux/netfilter.h`。

钩子通过 :c:type:`struct nf_hook_ops` 结构体进行定义:

.. code-block:: c

  struct nf_hook_ops {
  	/* 用户从这里开始填写。*/
  	nf_hookfn               *hook;
  	struct net_device       *dev;
  	void                    *priv;
  	u_int8_t                pf;
  	unsigned int            hooknum;
  	/* 钩子按优先级升序排列。*/
  	int                     priority;
  };

其中:

  * ``pf`` 是数据包类型 (``PF_INET`` 等);
  * ``priority`` 是优先级;优先级在 :file:`uapi/linux/netfilter_ipv4.h` 中定义如下:

.. code-block:: c

  enum nf_ip_hook_priorities {
  	NF_IP_PRI_FIRST = INT_MIN,
  	NF_IP_PRI_CONNTRACK_DEFRAG = -400,
  	NF_IP_PRI_RAW = -300,
  	NF_IP_PRI_SELINUX_FIRST = -225,
  	NF_IP_PRI_CONNTRACK = -200,
  	NF_IP_PRI_MANGLE = -150,
  	NF_IP_PRI_NAT_DST = -100,
  	NF_IP_PRI_FILTER = 0,
  	NF_IP_PRI_SECURITY = 50,
  	NF_IP_PRI_NAT_SRC = 100,
  	NF_IP_PRI_SELINUX_LAST = 225,
  	NF_IP_PRI_CONNTRACK_HELPER = 300,
  	NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX,
  	NF_IP_PRI_LAST = INT_MAX,
  };

\


  * ``dev`` 是捕获操作针对的设备(网络接口);

  * ``hooknum`` 是我们使用的钩子类型。当捕获到数据包时,处理模式由 ``hooknum`` 和 ``hook`` 字段定义。对于 IP,钩子类型在 :file:`linux/netfilter.h` 中定义:

.. code-block:: c

  enum nf_inet_hooks {
  	NF_INET_PRE_ROUTING,
  	NF_INET_LOCAL_IN,
  	NF_INET_FORWARD,
  	NF_INET_LOCAL_OUT,
  	NF_INET_POST_ROUTING,
  	NF_INET_NUMHOOKS
  };

\

  * ``hook`` 是在捕获网络数据包时调用的处理程序(数据包以 :c:type:`struct sk_buff` 结构体形式发送)。 ``private`` 字段是传递给处理程序的私有信息。捕获处理程序的原型由 :c:type:`nf_hookfn` 类型定义:

.. code-block:: c

  struct nf_hook_state {
  	unsigned int hook;
  	u_int8_t pf;
  	struct net_device *in;
  	struct net_device *out;
  	struct sock *sk;
  	struct net *net;
  	int (*okfn)(struct net *, struct sock *, struct sk_buff *);
  };

  typedef unsigned int nf_hookfn(void *priv,
  			       struct sk_buff *skb,
  			       const struct nf_hook_state *state);

捕获函数 :c:func:`nf_hookfn` 中, ``priv`` 参数是 :c:type:`struct nf_hook_ops` 初始化时传递的私有信息。 ``skb`` 是指向捕获的网络数据包的指针。根据 ``skb`` 的信息,可以进行数据包过滤决策。函数的 ``state`` 参数是与数据包捕获相关的状态信息,包括输入接口、输出接口、优先级和钩子号。优先级和钩子号可使同一个函数被多个钩子调用。

捕获处理程序可以返回以下常量之一 (``NF_*``):

.. code-block:: c

  /* hook 函数的响应结果 */
  #define NF_DROP 0
  #define NF_ACCEPT 1
  #define NF_STOLEN 2
  #define NF_QUEUE 3
  #define NF_REPEAT 4
  #define NF_STOP 5
  #define NF_MAX_VERDICT NF_STOP

``NF_DROP`` 用于过滤(忽略)数据包, ``NF_ACCEPT`` 用于接受数据包并将其转发。

通过使用在 :file:`linux/netfilter.h` 中定义的函数来注册/注销一个 hook:

.. code-block:: c

  /* 注册/注销 hook 点的函数 */
  int nf_register_net_hook(struct net *net, const struct nf_hook_ops *ops);
  void nf_unregister_net_hook(struct net *net, const struct nf_hook_ops *ops);
  int nf_register_net_hooks(struct net *net, const struct nf_hook_ops *reg,
  			  unsigned int n);
  void nf_unregister_net_hooks(struct net *net, const struct nf_hook_ops *reg,
  			     unsigned int n);


.. attention::

  在 Linux 内核 3.11-rc2 版本之前,作为 netfilter 钩子参数的 :c:type:`struct sk_buff` 结构,内部的头部提取函数有一些限制。虽然每次都可以使用 :c:func:`ip_hdr` 获取 IP 头部,但是用于获取 TCP 和 UDP 头部的 :c:func:`tcp_hdr` 和 :c:func:`udp_hdr` 函数,却只能对从系统内部,而非外部接收的数据包使用。对于后者的情况,需要手动计算数据包中的头部偏移量:

  .. code-block:: c

    // TCP 数据包 (iph->protocol == IPPROTO_TCP)
    tcph = (struct tcphdr*)((__u32*)iph + iph->ihl);
    // UDP 数据包 (iph->protocol == IPPROTO_UDP)
    udph = (struct udphdr*)((__u32*)iph + iph->ihl);

  这段代码适用于所有过滤场景,因此建议使用它来替代头部访问函数。

下面是一个 netfilter hook 的使用示例:

.. code-block:: c

  #include <linux/netfilter.h>
  #include <linux/netfilter_ipv4.h>
  #include <linux/net.h>
  #include <linux/in.h>
  #include <linux/skbuff.h>
  #include <linux/ip.h>
  #include <linux/tcp.h>

  static unsigned int my_nf_hookfn(void *priv,
  		struct sk_buff *skb,
  		const struct nf_hook_state *state)
  {
  	/* 处理数据包 */
  	//...

  	return NF_ACCEPT;
  }

  static struct nf_hook_ops my_nfho = {
  	.hook        = my_nf_hookfn,
  	.hooknum     = NF_INET_LOCAL_OUT,
  	.pf          = PF_INET,
  	.priority    = NF_IP_PRI_FIRST
  };

  int __init my_hook_init(void)
  {
  	return nf_register_net_hook(&init_net, &my_nfho);
  }

  void __exit my_hook_exit(void)
  {
  	nf_unregister_net_hook(&init_net, &my_nfho);
  }

  module_init(my_hook_init);
  module_exit(my_hook_exit);

netcat
======

在开发包含网络编程的应用程序时,常用的工具包括 netcat。它也被昵称为“TCP/IP 的瑞士军刀”。它可以用于以下功能:

  * 发起 TCP 连接;
  * 等待 TCP 连接;
  * 发送和接收 UDP 数据包;
  * 以十六进制转储(hexdump)格式显示流量;
  * 在建立连接后运行程序(例如,一个 shell);
  * 在发送的数据包中设置特殊选项。

发起 TCP 连接:

.. code-block:: console

  nc 主机名 端口号

监听 TCP 端口:

.. code-block:: console

  nc -l -p 端口号

发送和接收 UDP 数据包,需要添加 ``-u`` 命令行选项。

.. note::

  命令是 :command:`nc`;通常 :command:`netcat` 是此命令的别名。还有其他实现 netcat 命令的版本,其中一些与经典实现参数略有不同。运行 :command:`man nc` 或 :command:`nc -h` 以查看如何使用它。

有关 netcat 的更多信息,请参阅以下 `教程 <https://www.win.tue.nl/~aeb/linux/hh/netcat_tutorial.pdf>`_。

进一步阅读
===========

#. 了解 Linux 网络内部
#. `Linux IP 网络`_
#. `TUX Web 服务器`_
#. `Beej 的互联网套接字网络编程指南`_
#. `内核中的网络编程——Kernel Korner`_
#. `深入 Linux 内核网络堆栈`_
#. `netfilter.org 项目`_
#. `深入了解 Iptables 和 Netfilter 架构`_
#. `Linux 基金会网络页面`_

.. _Linux IP 网络: http://www.cs.unh.edu/cnrg/gherrin/
.. _TUX Web 服务器: http://www.stllinux.org/meeting_notes/2001/0719/myTUX/
.. _Beej 的互联网套接字网络编程指南: https://www.beej.us/guide/bgnet/
.. _内核中的网络编程——Kernel Korner: http://www.linuxjournal.com/article/7660
.. _深入 Linux 内核网络堆栈: http://phrack.org/issues/61/13.html
.. _netfilter.org 项目: http://www.netfilter.org/
.. _深入了解 Iptables 和 Netfilter 架构: https://www.digitalocean.com/community/tutorials/a-deep-dive-into-iptables-and-netfilter-architecture
.. _Linux 基金会网络页面: http://www.linuxfoundation.org/en/Net:Main_Page

练习
====

.. include:: ../labs/exercises-summary.hrst
.. |LAB_NAME| replace:: networking

.. important::

  你需要确保内核支持 ``netfilter``。可以通过 ``CONFIG_NETFILTER`` 来启用它。要激活它,请在 :file:`linux` 目录中运行 :command:`make menuconfig`,并在 ``Networking support -> Networking options`` 中勾选 ``Network packet filtering framework (Netfilter)`` 选项。如果它未启用,请启用它(作为内置功能,而不是外部模块——必须带有 ``*`` 标记)。


1. 在内核空间中显示数据包
------------------------

编写一个内核模块,显示发起出站连接的 TCP 数据包的源地址和端口。从 :file:`1-2-netfilter` 中的代码开始,并填写标有 ``TODO 1`` 的区域,同时考虑下面的注释。

你需要注册一个类型为 ``NF_INET_LOCAL_OUT`` 的 netfilter 钩子,如 `netfilter`_ 部分所述。

借助 `struct sk_buff 结构`_, 你可以使用特定的函数访问数据包头部。:c:func:`ip_hdr` 函数以返回指向 :c:type:`struct iphdr` 结构的指针的形式,返回 IP 头部。:c:func:`tcp_hdr` 函数以返回指向 :c:type:`struct tcphdr` 结构的指针的形式,返回 TCP 头部。

`图表`_ 解释了如何建立 TCP 连接。连接初始化数据包在 TCP 头部中设置了 ``SYN`` 标志,并清除了 ``ACK`` 标志。

.. note::

  要显示源 IP 地址,请使用 printk 函数的 ``%pI4`` 格式。详细信息可以在 `内核文档 <https://www.kernel.org/doc/Documentation/printk-formats.txt>`_ (``IPv4 addresses`` 部分)中找到。以下是使用 ``%pI4`` 的示例代码片段:

  .. code-block:: c

    printk("IP address is %pI4\n", &iph->saddr);

  在使用 ``%pI4`` 格式时,printk 的实参是指针。因此,构造应为 ``&iph->saddr`` (带有 & 运算符)而不是 ``iph->saddr``。

在 TCP 头部中,源 TCP 端口以 `网络字节序`_ 格式表示。请阅读 :ref:`转换` 部分。使用 :c:func:`ntohs` 进行转换。

为了进行测试,请使用 :file:`1-2-netfilter/user/test-1.sh` 文件。该测试创建一个到本地主机的连接,然后由内核模块拦截和显示该连接。 :command:`make copy` 命令仅会在该脚本标记为可执行的情况下,才会将该脚本复制到虚拟机上。该脚本使用静态编译得到的 :command:`netcat` 工具,该工具的路径是 :file:`skels/networking/netcat`;此程序必须具有执行权限。

运行检查器后,输出应类似于下面的示例:

.. code-block:: c

  # ./test-1.sh
  [  229.783512] TCP connection initiated from 127.0.0.1:44716
  Should show up in filter.
  Check dmesg output.

2. 按目标地址进行过滤
---------------------

扩展练习 1 中的模块,以便你可以通过 ``MY_IOCTL_FILTER_ADDRESS`` ioctl 调用指定目标地址。注意只显示包含指定目标地址的数据包。为了解决这个任务,填写标有 ``TODO 2`` 的区域,并按照以下规范进行操作。

要实现 ioctl 例程,你必须填写 ``my_ioctl`` 函数。请查看 :ref:`ioctl` 部分的内容。从用户空间发送的地址使用 `网络字节序`_, 因此 **无需** 进行转换。

.. note::

  通过 ``ioctl`` 发送的 IP 地址是通过地址发送的,而不是通过值发送的。地址必须存储在 ``ioctl_set_addr`` 变量中。可以使用 :c:func:`copy_from_user` 进行复制。

要比较地址,请填写 ``test_daddr`` 函数。这里我们无需转换地址,即可将(使用网络字节序的)地址进行比较(如果从左到右相等,则反转后也相等)。

如果要显示按目标地址过滤出的连接初始化数据包(这些过滤出的数据包,其目标地址与我们通过 ioctl 例程发送的地址相符),那么 ``test_daddr`` 函数必须在 netfilter 钩子中调用。连接初始化数据包在 TCP 头部中设置了 ``SYN`` 标志,并清除了 ``ACK`` 标志。你需要检查两件事情:

  * TCP 标志;
  * 数据包的目标地址(使用 ``test_addr``)。

为了进行测试,请使用 :file:`1-2-netfilter/user/test-2.sh` 脚本。此脚本需要编译 :file:`1-2-netfilter/user/test.c` 文件以生成测试可执行文件。在物理系统上运行 :command:`make build` 命令时,会自动进行编译。只有标记为可执行,该测试脚本才会复制到虚拟机上。该脚本使用静态编译的 :command:`netcat` 工具(在 :file:`skels/networking/netcat` 中);该可执行文件必须具有执行权限。

运行检查器后,输出应类似于下面的示例:

.. code-block:: console

  # ./test-2.sh
  [  797.673535] TCP connection initiated from 127.0.0.1:44721
  Should show up in filter.
  Should NOT show up in filter.
  Check dmesg output.

测试首先要求对 ``127.0.0.1`` IP 地址进行数据包过滤,然后对 ``127.0.0.2`` IP 地址进行过滤。第一个连接初始化数据包(到 ``127.0.0.1``)被过滤器拦截并显示,而第二个数据包(到 ``127.0.0.2``)则未被拦截。

3. 监听 TCP socket
-------------------

编写一个内核模块,在回环接口(loopback interface)(在 ``init_module`` 中)上创建监听连接的 TCP 套接字,监听端口为 ``60000``。从 :file:`3-4-tcp-sock` 中的代码开始,填写标有 ``TODO 1`` 的区域,同时考虑以下观察结果。

请阅读 `对 socket 结构的操作`_ 和 `struct proto_ops 结构`_ 部分。

``sock`` socket 是 ``服务器套接字``,因此必须处于监听状态。也就是说,必须对该 socket 执行 ``bind`` 和 ``listen`` 操作。在内核空间中,要执行类似于 ``bind`` 和 ``listen`` 的操作,你需要调用类似 ``sock->ops->...;`` 的函数。你可以调用的示例函数包括 ``sock->ops->bind``, ``sock->ops->listen`` 等。

.. note::

  例如,调用 ``sock->ops->bind`` 或 ``sock->ops->listen`` 函数,查看在 :c:func:`sys_bind` 和 :c:func:`sys_listen` 系统调用处理程序中调用它们的方式。

  在 Linux 内核源代码树的 ``net/socket.c`` 文件中,查找系统调用处理程序。

.. note::

  对于 ``listen`` 的第二个参数(backlog),请使用 ``LISTEN_BACKLOG``。

在模块的退出函数和标有错误标签的区域中记得释放 socket;可以使用 :c:func:`sock_release` 来释放。

要进行测试,请运行 :command:`3-4-tcp_sock/test-3.sh` 脚本。只有在标记为可执行时,该脚本才会通过 :command:`make copy` 复制到虚拟机上。

运行测试后,将显示一个 TCP 套接字,通过监听端口 ``60000`` 进行连接。

4. 在内核空间接受连接
--------------------

扩展上一个练习中的模块,以允许外部连接(无需发送任何消息,只需接受新连接)。填写标有 ``TODO 2`` 的区域。

请阅读 `对 socket 结构的操作`_ 和 `struct proto_ops 结构`_ 部分。

对于内核空间的 ``accept`` 等效操作,请参阅 :c:func:`sys_accept4` 系统调用处理程序。请查看 `lnet_sock_accept <https://elixir.bootlin.com/linux/v4.17/source/drivers/staging/lustre/lnet/lnet/lib-socket.c#L511>`_ 实现,以及如何使用 ``sock->ops->accept`` 调用。将倒数第二个参数 (``flags``) 的值设为 ``0``,将最后一个参数 (``kern``) 的值设为 ``true``。

.. note::

  在 Linux 内核源代码树的 ``net/socket.c`` 文件中查找系统调用处理程序。

.. note::

  必须使用 :c:func:`sock_create_lite` 函数创建新套接字 (``new_sock``),然后使用以下方式配置其操作:

  .. code-block:: console

    newsock->ops = sock->ops;

打印目标 socket 的地址和端口。要查找 socket 的对等名称(即地址),请参考 :c:func:`sys_getpeername` 系统调用处理程序。

.. note::

  ``sock->ops->getname`` 函数的第一个参数是连接套接字,即通过 ``accept`` 调用初始化的 ``new_sock``。

  ``sock->ops->getname`` 函数的最后一个参数是 ``1``,表示我们想要了解有关端点或对等点 (*远程端* 或 *对等点*) 的信息。

  使用文件中定义的 ``print_sock_address`` 宏显示对等地址(由 ``raddr`` 变量指示)。

在模块的退出函数中,以及在错误标签之后,释放新创建的套接字(接受连接后)。在将 ``accept`` 代码添加到模块初始化函数之后,:command:`insmod` 操作将会阻塞,直到建立连接。你可以使用 :command:`netcat` 在该端口上解锁。因此,上一个练习中的测试脚本将无法工作。

要进行测试,请运行 :file:`3-4-tcp_sock/test-4.sh` 脚本。只有在标记为可执行时,该脚本才会通过 :command:`make copy` 复制到虚拟机上。

不会显示任何特殊内容(在内核缓冲区中)。测试的成功将由连接的建立来确定。然后使用 ``Ctrl+c`` 停止测试脚本,然后可以移除内核模块。

5. UDP 套接字发送方
-------------------

编写一个内核模块,其创建一个 UDP socket,并将来自 socket 的 ``MY_TEST_MESSAGE`` 宏的消息发送到回环地址的端口 ``60001``。

从 :file:`5-udp-sock` 中的代码开始。

请阅读 `对 socket 结构的操作`_ 和 `struct proto_ops 结构`_ 部分。

要了解如何在内核空间中发送消息,请参阅 :c:func:`sys_send` 系统调用处理程序或 `发送/接收消息`_。

.. hint::

  :c:type:`struct msghdr` 结构的 ``msg_name`` 字段必须初始化为目标地址(指向 :c:type:`struct sockaddr` 的指针), ``msg_namelen`` 字段必须初始化为地址大小。

  将 :c:type:`struct msghdr` 结构的 ``msg_flags`` 字段初始化为 ``0``。

  将 :c:type:`struct msghdr` 结构的 ``msg_control`` 和 ``msg_controllen`` 字段分别初始化为 ``NULL`` 和 ``0``。

要发送消息,请使用 :c:func:`kernel_sendmsg`。

消息传输参数从内核空间中检索。在 :c:func:`kernel_sendmsg` 调用中,将 :c:type:`struct iovec` 结构指针转换为 :c:type:`struct kvec` 指针。

.. hint::

  :c:func:`kernel_sendmsg` 的最后两个参数分别为 ``1`` (I/O 向量的数量) 和 ``len`` (消息大小)。

要进行测试,请使用 :file:`test-5.sh` 脚本。只有在标记为可执行时,该脚本才会通过 :command:`make copy` 复制到虚拟机上。该脚本使用存储在 :file:`skels/networking/netcat` 中的静态编译的 ``netcat`` 工具;该可执行文件必须具有执行权限。

如果正确实现,运行 :file:`test-5.sh` 脚本后,将显示类似下面的输出中的 ``kernelsocket`` 消息:

.. code-block:: console

  /root # ./test-5.sh
  + pid=1059
  + sleep 1
  + nc -l -u -p 60001
  + insmod udp_sock.ko
  kernelsocket
  + rmmod udp_sock
  + kill 1059