惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 叶小钗
O
OpenAI News
V
V2EX
大猫的无限游戏
大猫的无限游戏
博客园 - 聂微东
S
Schneier on Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
小众软件
小众软件
L
LINUX DO - 热门话题
C
Cybersecurity and Infrastructure Security Agency CISA
博客园 - Franky
Security Latest
Security Latest
S
SegmentFault 最新的问题
Project Zero
Project Zero
Spread Privacy
Spread Privacy
K
Kaspersky official blog
J
Java Code Geeks
V
Vulnerabilities – Threatpost
C
Cisco Blogs
C
CERT Recently Published Vulnerability Notes
月光博客
月光博客
T
The Exploit Database - CXSecurity.com
L
Lohrmann on Cybersecurity
人人都是产品经理
人人都是产品经理
博客园 - 三生石上(FineUI控件)
Scott Helme
Scott Helme
WordPress大学
WordPress大学
量子位
T
Threat Research - Cisco Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
宝玉的分享
宝玉的分享
Hugging Face - Blog
Hugging Face - Blog
AWS News Blog
AWS News Blog
Help Net Security
Help Net Security
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Simon Willison's Weblog
Simon Willison's Weblog
S
Secure Thoughts
博客园 - 【当耐特】
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
V
Visual Studio Blog
Last Week in AI
Last Week in AI
T
Tailwind CSS Blog
腾讯CDC
Cyberwarzone
Cyberwarzone
IT之家
IT之家
GbyAI
GbyAI
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
云风的 BLOG
云风的 BLOG
T
Troy Hunt's Blog
D
Docker

郑文峰的博客

使用dify对接飞书多维表格 使用n8n对接飞书多维表格 服务启动时出现 OOM 一次服务升级时pg表DDL执行超时失败 Go语言高效IO缓冲技术详解 Go语言延迟初始化(Lazy Initialization)最佳实践 Go语言字符串拼接性能对比与优化指南 Go语言结构体内存对齐完全指南 Go语言空结构体:零内存消耗的高效编程 Go语言堆栈分配与逃逸分析深度解析 Go语言原子操作完全指南 Go语言内存预分配完全指南 Go语言不可变数据共享:无锁并发编程实践 Go语言零拷贝技术完全指南 Go语言遍历性能深度解析:从原理到优化实践 Go语言Interface Boxing原理与性能优化指南 Go协程池深度解析:原理、实现与最佳实践 使用etcd分布式锁导致的协程泄露与死锁问题 基于pre-commit的Python代码规范落地实践 初识 MCP Server pulsar阻塞导致logstash无法接入日志 django-prometheus使用及源码分析 kube-proxy源码分析 kubernetes service如何通过iptables转发 tcp缓存引起的日志丢失 django-apschedule定时任务异常停止 理解calico容器网络通信方案原理 理解flannel的三种容器网络方案原理 理解Linux IPIP隧道 理解VXLAN网络 快速了解iptables kafka中listener和advertised.listeners的作用 django rest_framework 分页 django后端服务、logstash和flink接入VictoriaMetrics指标监控 python中import原理 docker容器单机网络 手动实现docker容器bridge网络模型 mysql之MVCC原理 mysql之日志 使用java开发logstash的filter插件 使用python实现单例模式的三种方式 redis之缓存 redis之分片集群 redis之哨兵机制 redis之主从库同步 redis之持久化 redis之五种基本数据类型 go中如何处理error pod中将代码与运行环境分离 ddt源码分析 python装饰器的使用方法 读书笔记:如何阅读一本书 使用ddt实现unittest的参数化测试 使用kubeadm安装k8s 优化gin表单的错误提示信息 gin中validator模块的源码分析 go简单使用grpc python简单使用grpc k8s之PV、PVC和StorageClass k8s之StatefulSet k8s之DaemonSet k8s之Job和CronJob k8s之ConfigMap和Secret k8s之Service k8s之Pod k8s之Deployment 容器的本质 docker容器 python迭代器与生成器 python元编程 python垃圾回收机制 python上下文管理器 django rest_framework使用jwt django rest_framework异常处理 django rest_framework 自定义文档 django压缩文件下载 django rest_framework使用pytest单元测试 django restframework choice 自定义输出数据 django Filtering 使用 django viewset 和 Router 配合使用时报的错 django model的序列化 django中使用AbStractUser django.core.exceptions.ImproperlyConfigured Application labels aren't unique, duplicates users django 中 media配置 django 外键引用自身和on_delete参数 django 警告 while time zone support is active Flask使用flask_socketio实现websocket flask结合mongo tornado 文件上传 tornado 使用jwt完成用户异步认证 tornado 用户密码 bcrypt加密 tornado 结合wtforms使用表单操作 tornado finish和write区别 tornado 使用peewee-async 完成异步orm数据库操作 pyspark streaming简介 和 消费 kafka示例 使用hue创建ozzie的pyspark action workflow count的性能优化 django rest_framework Authentication django celery 结合使用 网站
理解Linux TunTap设备
zhengwenfeng · 2023-05-26 · via 郑文峰的博客

# 入门

TUN/TAP是操作系统内核中的虚拟网络设备,可以完成用户空间与内核空间的数据的交互。网络协议栈中的数据通过该设备可以进入到用户空间中,而用户空间中的程序通过该设备空间进入到内核空间的网络协议栈。

TUN模拟的是三层设备,操作三层的数据包,而TAP模拟的二层设备,操作二层的数据包。

物理网卡与虚拟网卡的区别是,物理网卡是外界与内核空间的网络协议栈数据交互的门户,而虚拟网卡是用户空间和内核空间交互的门户。

/dev/net/tun​是linux提供的字符设备,写入该设备的数据会发送到虚拟网卡中,而发送到虚拟网卡中的数据也会出现在字符设备中。

# 应用程序通过tun设备获取ping数据包

app程序通过打开tun字符设备创建出tun虚拟网卡。然后通过ping命令发送ICMP数据包到网络协议栈中,这个过程是从用户空间到内核空间,再通过路由将数据包转发到tun虚拟网卡中,因为tun网卡特性,会进入到打开该tun设备用户空间app程序中。

app程序代码如下:

import os
import struct
from fcntl import ioctl

BUFFER_SIZE = 4096

# 完成虚拟网卡的注册
TUNSETIFF = 0x400454ca

# 设备模式
IFF_TUN = 0x0001
IFF_TAP = 0x0002


def create_tunnel(tun_name='tun%d', tun_mode=IFF_TUN):
    # 以读写的方式打开字符设备tun,获取到设备描述符
    tun_fd = os.open("/dev/net/tun", os.O_RDWR)

    # 对该设备进行配置,设备名称和设备模式。
    ifn = ioctl(tun_fd, TUNSETIFF, struct.pack(b"16sH", tun_name.encode(), tun_mode))

    # 获取到设备名称
    tun_name = ifn[:16].decode().strip("\x00")
    return tun_fd, tun_name


def main():
    tun_fd, tun_name = create_tunnel()

    while True:
        data = os.read(tun_fd, BUFFER_SIZE)
        print(f"get data from tun. data size = {len(data)}")


if __name__ == '__main__':
    main()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

运行后输出:

# python3 tun_demo.py
Open tun/tap device: tun0 for reading...

1
2

通过ip a​命令发现tun设备已经创建,但其状态为DOWN

# ip a | grep -C tun
45: tun0: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc noop state DOWN group default qlen 500
    link/none

1
2
3

对其设置一个ip并将它状态设置为UP

ip a add 192.37.1.2/24 dev tun0
ip link set tun0 up

1
2

配置好ip后,会发现自动配置了如下路由:

...
192.37.1.0/24 dev tun0 proto kernel scope link src 192.37.1.2 
...

1
2
3

再次查看tun设备,发现已经配置好

# ip a | grep tun0
45: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 500
    inet 192.37.1.1/24 scope global tun0

1
2
3

并且这时会发现tun0已经接收到了3个数据包

# python3 tun_demo.py
Open tun/tap device: tun0 for reading...
get data from tun. data size = 52
get data from tun. data size = 52
get data from tun. data size = 52

1
2
3
4
5

这时候使用tcpdump监听tun0,执行ping 192.37.1.2,是有回包的,但是tcpdump却没有抓到任何包。

ping命令会根据目标IP地址和子网掩码来判断数据包的目的地,如果目的地在本地网络中,ping命令会直接将数据包发送到本地网络,而不是通过TUN设备发送。

# tcpdump -i tun0 -n
...

# ping 192.37.1.2 -c 3
PING 192.37.1.2 (192.37.1.2) 56(84) bytes of data.
64 bytes from 192.37.1.2: icmp_seq=1 ttl=64 time=0.148 ms
64 bytes from 192.37.1.2: icmp_seq=2 ttl=64 time=0.114 ms
64 bytes from 192.37.1.2: icmp_seq=3 ttl=64 time=0.316 ms

--- 192.37.1.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.114/0.192/0.316/0.089 ms

1
2
3
4
5
6
7
8
9
10
11
12
13

改为ping 192.37.1.3,可以看到程序收到了收到了数据包,tcpdump也抓到了包,但是因为没有做任何的处理也没有回包,所以ping命令看到不到回包。

# python3 tun_demo.py
Open tun/tap device: tun0 for reading...
get data from tun. data size = 52
get data from tun. data size = 52
get data from tun. data size = 52

get data from tun. data size = 88
get data from tun. data size = 88
get data from tun. data size = 88

# tcpdump -i tun0 -n
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tun0, link-type RAW (Raw IP), capture size 262144 bytes
22:56:41.018149 IP 192.37.1.2 > 192.37.1.3: ICMP echo request, id 22559, seq 1, length 64
22:56:42.018871 IP 192.37.1.2 > 192.37.1.3: ICMP echo request, id 22559, seq 2, length 64
22:56:43.022732 IP 192.37.1.2 > 192.37.1.3: ICMP echo request, id 22559, seq 3, length 64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 使用tun设备完成基于UDP的容器跨节点通信

使用tun设备基于UDP完成容器跨节点通信。如下图所示:

通信流程是,在Node1中的NS1进行ping Node2中NS2的veth0网卡的IP,ICMP的IP包会通过veth0到达veth1中,并进入到宿主机的网络协议栈,通过路由配置达到tun设备,这时app服务从tun设备中读取到IP包数据,然后将其封装在UDP包中,并通过eth0网卡发送到Node2的eth0网卡上,通过网络协议栈解包达到app程序中,拿到里面的IP包,将其写入到tun设备中,进入到网络协议栈中,通过路由达到veth1中,然后到达net ns1的veth0网卡。

app程序简单实现如下:

import os
import socket
import struct
import threading
from fcntl import ioctl
import click

BIND_ADDRESS = ('0.0.0.0', 7000)
BUFFER_SIZE = 4096

TUNSETIFF = 0x400454ca
IFF_TUN = 0x0001
IFF_TAP = 0x0002


def create_tunnel(tun_name='tun%d', tun_mode=IFF_TUN):
    tun_fd = os.open("/dev/net/tun", os.O_RDWR)
    ifn = ioctl(tun_fd, TUNSETIFF, struct.pack(b"16sH", tun_name.encode(), tun_mode))
    tun_name = ifn[:16].decode().strip("\x00")
    return tun_fd, tun_name


def start_tunnel(tun_name):
    os.popen(f"ip link set {tun_name} up")


def udp_server(udp_socket, tun_fd):
    while True:
        data, addr = udp_socket.recvfrom(2048)
        print("get data from udp.")
        if not data:
            break

        os.write(tun_fd, data)


@click.command()
@click.option("--peer_node_ip", "-p", required=True, help="对端节点IP")
def main(peer_node_ip):
    peer_node_addr = (peer_node_ip, 7000)

    tun_fd, tun_name = create_tunnel()
    start_tunnel(tun_name)

    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    udp_socket.bind(BIND_ADDRESS)

    t = threading.Thread(target=udp_server, args=(udp_socket, tun_fd))
    t.daemon = True
    t.start()

    while True:
        data = os.read(tun_fd, BUFFER_SIZE)
        print(f"get data from tun. data size = {len(data)}")
        udp_socket.sendto(data, peer_node_addr)


if __name__ == '__main__':
    main()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

在Node1中运行该程序,设置Node2 IP

python3 tun_app.py -p 10.65.132.187

1

可以看到已经创建了tun设备

# ip link show tun0
...
109: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 500
    link/none 
    inet6 fe80::8e98:91a4:6537:d77a/64 scope link flags 800 
       valid_lft forever preferred_lft forever
...

1
2
3
4
5
6
7

在Node1中创建Network Namespace命名为net1,使用它来完成模拟容器网络。

然后创建veth pair,它们是一对网卡,分别为命名为veth0和veth1

ip link add veth0 type veth peer name veth1

1

将其一端接入到net1中,并设置好其IP地址为10.1.1.2/24

ip link set dev veth0 netns net1
ip netns exec net1 ip addr add 10.1.1.2/24 dev veth0
ip netns exec net1 ip link set dev veth0 up

1
2
3

开启在宿主机上的veth1网卡,并设置其IP为10.1.1.1/24

ip a add 10.1.1.1/24 dev veth1
ip link set dev veth1 up

1
2

再将net1中的默认路由设置成都走veth0,这样,ping Node2中net2的网络包可以到veth1中,也就进入到了宿主机的网络协议栈中。

ip netns exec net1 ip r add default via 10.1.1.1 dev veth0

1

在宿主机上还需要添加路由,访问Node2中net2时都路由到tun0设备

ip r add 10.1.2.0/24 dev tun0

1

这时,在Node1 net1中ping Node2 net2时,正常来说是可以在app中看到从tun收到IP包的,虽然没有回包,那是因为app程序收到包后没有做任何回包操作。

# ip netns exec net1 ping 10.1.2.2 -c 3
PING 10.1.2.2 (10.1.2.2) 56(84) bytes of data.
--- 10.1.2.2 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2001ms

1
2
3
4

我们通过tcpdump抓取veth1网卡,可以看到收到了ARP请求,想要获取10.1.2.2的MAC地址,但是一直获取不到,所以导致IP包无法通过路由达到TUN设备

# tcpdump -i veth1 -n
00:45:13.988076 ARP, Request who-has 10.1.2.2 tell 10.1.1.2, length 28

1
2

这个时候需要开启veth1的arp代理,将veth1的MAC地址作为ARP的回复。

echo 1 >  /proc/sys/net/ipv4/conf/veth1/proxy_arp

1

再次ping Node2 net2时,可以看到tcpdump看到ARP中回复的MAC地址为veth1的地址。

# tcpdump -i veth1 -n
00:45:13.988076 ARP, Request who-has 10.1.2.2 tell 10.1.1.2, length 28
00:45:13.988100 ARP, Reply 10.1.2.2 is-at 4e:7c:bf:fe:4d:0f, length 28

# ip a | grep -C 3 veth1
...
107: veth1@if108: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 4e:7c:bf:fe:4d:0f brd ff:ff:ff:ff:ff:ff link-netnsid 3
    inet 10.1.1.1/24 scope global veth1
       valid_lft forever preferred_lft forever
...

1
2
3
4
5
6
7
8
9
10
11

并且app程序中也从tun设备中获取到了IP包。

# python3 tun_app.py -p 10.65.132.187
get data from tun. data size = 52
get data from tun. data size = 52
get data from tun. data size = 52
get data from tun. data size = 88
get data from tun. data size = 88
get data from tun. data size = 88

1
2
3
4
5
6
7

到这一步,Node1的基本配置完成,接下来配置Node2,配置的方法与Node1一致,在Node2执行命令如下:

# 开启app程序
python3 tun_app.py -p 10.61.74.37

# 新增network namespace net2
ip netns add net2

# 新增veth pair设备
ip link add veth0 type veth peer name veth1

# 配置veth pair设备
ip link set dev veth0 netns net2
ip netns exec net2 ip addr add 10.1.2.2/24 dev veth0
ip netns exec net2 ip link set dev veth0 up

ip a add 10.1.2.1/24 dev veth1
ip link set dev veth1 up

# 添加默认路由
ip netns exec net2 ip r add default via 10.1.2.1 dev veth0

# 添加tun0设备路由
ip r add 10.1.1.0/24 dev tun0

# 开启arp代理
echo 1 >  /proc/sys/net/ipv4/conf/veth1/proxy_arp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

配置完成后,在Node1的net1中ping Node2的net2,可以ping通有回包。

# ip netns exec net1 ping 10.1.2.2 
PING 10.1.2.2 (10.1.2.2) 56(84) bytes of data.
64 bytes from 10.1.2.2: icmp_seq=1 ttl=62 time=5.46 ms
64 bytes from 10.1.2.2: icmp_seq=2 ttl=62 time=4.67 ms
64 bytes from 10.1.2.2: icmp_seq=3 ttl=62 time=5.52 ms

1
2
3
4
5

# 巨人的肩膀