我知道在 Linux 上从现有网络接口写入和接收网络数据包的两种主要方法。第一种是使用经典套接字 API,其中 Linux 内核负责管理我发送和接收的数据的所有 TCP/IP 封装。第二种是使用 AF_PACKET 接口,我在其中读取和写入原始 IP(甚至以太网)数据包到接口。我搁置了 TAP/TUN 设备、veth 对、网桥等,因为在我的用例中我无法创建任何新的网络接口,只能使用现有的接口。

使用 AF_PACKET 方法,如果我想发送和接收数据并参与“真实”的 TCP 连接,似乎我必须或多或少地接管整个接口的控制权,因为如果我写入一个 SYN 数据包,并在响应中收到 SYNACK 数据包,那么即使我通过解码 AF_PACKET 套接字上的数据包来处理 SYNACK 数据包,Linux 内核也会处理 SYNACK 数据包,可能以某种方式响应,甚至将其路由到现有的套接字 API 级连接。

我想知道的是:如果我实现自己的 TCP 堆栈,是否有任何方法可以通过 Linux 内核主机接口上的端口进行通信,同时允许其他进程仍在同一主机接口上使用普通套接字 API?我想对 Linux 内核说的是:“嘿,请给我一个您不使用的 TCP 端口,从现在开始,如果您收到往返于该端口的数据包,请忽略它们”,然后我使用我的 AF_PACKET 套接字通过这个分配的端口写入 TCP 流量。

现在,我当然可以编写一个 nftables 规则来丢弃到某个端口的 TCP 流量。这样做的问题是:如何找到未被其他进程使用的端口,以及如何在我的进程退出时可靠地清理规则?我发现,nftables 规则很难以一种可靠的方式设置在已经具有复杂 nftables 设置的主机上,并与安装了自己的 nftables 规则的 docker 等服务进行互操作。

1

  • 看起来更像是一个 SO 问题…


    – 


最佳答案
1

我认为这正是tun/tap设备的用途;这些是由应用程序控制的虚拟网络设备。tap设备是第 2 层(因此您可以实现非 IP 协议),tun设备是第 3 层。

所以:

  • ip link使用/ etc配置接口,ip addr以便您可以将流量传递给它。例如:

    ip addr add 192.168.43.1/24 dev tun0
    ip link set tun0 up
    
  • 将流量引导至192.168.43.0/24网络上的地址:

    nc 192.168.43.10 100
    
  • 您的应用程序将从您在第一步收到的文件描述符中读取数据,并且也可以写回数据。

下面是创建tun接口、从中读取数据并在标准输出上进行十六进制转储的示例代码:

#include <errno.h>
#include <fcntl.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

int tun_alloc(char *dev) {
  struct ifreq ifr;
  int fd, err;

  if ((fd = open("/dev/net/tun", O_RDWR)) < 0) {
    fprintf(stderr, "failed to open /dev/net/tun: %d: %s\n", fd,
            strerror(errno));
    return -1;
  }

  memset(&ifr, 0, sizeof(ifr));

  /* Flags: IFF_TUN   - TUN device (no Ethernet headers)
   *        IFF_TAP   - TAP device
   *
   *        IFF_NO_PI - Do not provide packet information
   */
  ifr.ifr_flags = IFF_TUN;
  if (*dev)
    strncpy(ifr.ifr_name, dev, IFNAMSIZ);

  if ((err = ioctl(fd, TUNSETIFF, (void *)&ifr)) < 0) {
    close(fd);
    fprintf(stderr, "failed to acquire interface: %d: %s\n", fd,
            strerror(errno));
    return err;
  }
  strcpy(dev, ifr.ifr_name);
  return fd;
}

// via https://gist.github.com/ccbrown/9722406
void hexdump(const void *data, size_t size) {
  char ascii[17];
  size_t i, j;
  ascii[16] = '\0';
  for (i = 0; i < size; ++i) {
    printf("X ", ((unsigned char *)data)[i]);
    if (((unsigned char *)data)[i] >= ' ' &&
        ((unsigned char *)data)[i] <= '~') {
      ascii[i % 16] = ((unsigned char *)data)[i];
    } else {
      ascii[i % 16] = '.';
    }
    if ((i + 1) % 8 == 0 || i + 1 == size) {
      printf(" ");
      if ((i + 1) % 16 == 0) {
        printf("|  %s \n", ascii);
      } else if (i + 1 == size) {
        ascii[(i + 1) % 16] = '\0';
        if ((i + 1) % 16 <= 8) {
          printf(" ");
        }
        for (j = (i + 1) % 16; j < 16; ++j) {
          printf("   ");
        }
        printf("|  %s \n", ascii);
      }
    }
  }
}

int main() {
  char name[IFNAMSIZ + 1], cmd[1024];
  int fd;

  fd = tun_alloc(name);
  if (fd < 0) {
    fprintf(stderr, "tunnel allocation failed\n");
    exit(1);
  }

  fprintf(stderr, "got interface: %s\n", name);
  snprintf(cmd, sizeof(cmd), "ip addr add 192.168.43.1/24 dev %s", name);
  if (system(cmd) != 0) {
    fprintf(stderr, "failed to configure interface\n");
    exit(1);
  }
  snprintf(cmd, sizeof(cmd), "ip link set %s up", name);
  if (system(cmd) != 0) {
    fprintf(stderr, "failed to configure interface\n");
    exit(1);
  }

  while (1) {
    char buf[8192];
    int nb;

    nb = read(fd, buf, sizeof(buf));
    if (nb == 0) {
      break;
    } else if (nb < 0) {
      fprintf(stderr, "read failed: %s\n", strerror(errno));
      exit(1);
    }

    printf("read %d bytes\n", nb);
    hexdump(buf, nb);
  }

  return 0;
}

如果我们运行该程序(请注意,您必须以 身份运行该程序root)并尝试连接到地址192.168.43.10

nc 192.168.43.10 100

我们看到的输出:

read 64 bytes
00 00 08 00 45 00 00 3C  7B B8 40 00 40 06 E7 A7  |  ....E..<{.@.@...
C0 A8 2B 01 C0 A8 2B 0A  E5 28 00 64 7C 20 6C 3D  |  ..+...+..(.d| l=
00 00 00 00 A0 02 FA F0  82 47 00 00 02 04 05 B4  |  .........G......
04 02 08 0A C6 5E 5F 22  00 00 00 00 01 03 03 07  |  .....^_"........

根据,这是两个字节的标志、两个字节的协议信息,然后是原始协议数据。因此,剥离 tun 设备标头后,我们得到:

4500 003c e859 4000 4006 7b06 c0a8 2b01  E..<.Y@.@.{...+.
c0a8 2b0a b63c 0016 4710 5595 0000 0000  ..+..<..G.U.....
a002 faf0 6928 0000 0204 05b4 0402 080a  ....i(..........
c654 f33d 0000 0000 0103 0307            .T.=........

这是标准 IPv4 数据包。我们可以让应用程序对其进行解码并生成适当的响应。

笔记:

  1. 您可能还会看到其他输出,因为大多数系统通过所有可用接口执行各种网络服务公告/发现。
  2. 如果您不想要初始数据包元数据(标志和协议信息),那么您可以IFF_NO_PI在制作时进行设置TUNSETIFF ioctl;在这种情况下,您将只收到原始 IP 数据。