导语
尝试配合 gpts 写一些总结, 效果似乎没我更好 😢😢😢
简述
tun/tap 设备是 Linux 内核提供的虚拟网络设备。它们允许用户空间程序像处理物理网络设备一样,处理网络数据包。其中:
- tun (network TUNnel) 设备模拟了基于 IP 的网络设备,操作第三层数据包如 IP 数据包。
- tap (network tap) 设备模拟了以太网设备,操作第二层数据包如以太网数据帧。
使用 tun/tap 设备,可以实现许多功能,例如:
- 虚拟专用网络 (VPN): 利用 tun 设备,可以实现点对点 VPN,加密并通过公网隧道传输两端的网络流量。
- 网络流量监控: tap 设备可以配置为混杂 (promiscuous) 模式,对网络上所有经过它的数据包进行监听和分析。
- 协议模拟仿真: 利用 tun/tap,可以在用户空间实现一些自定义或新的网络协议栈,如一些物联网通信协议等。
- 网络功能虚拟化: tun/tap 是实现 OpenFlow, Open vSwitch 等软件定义网络方案的基础。
使用
使用 tun/tap 设备,需要内核支持,并加载相应的内核模块. 一般都支持了.
命令行: ip tuntap
命令创建 tun/tap 设备
1 2 3 4 5
| ip tuntap add dev tun0 mode tun
ip tuntap add dev tap0 mode tap ip addr add 192.168.1.10/24 dev tap0
|
编程:
- 创建 tun/tap 设备:
open
系统调用访问 /dev/net/tun
来创建。 - 配置设备:使用
ioctl
系统调用来配置新创建的设备,如分配设备名、设置设备模式(tun 或 tap)等。 - 设置网络参数:为 tun/tap 设备分配 IP 地址、路由等,可以使用
ifconfig
或 ip
命令。 - 数据读写:通过读写文件描述符来接收发送数据。
N 多语言都有非常良好的包装库, 使用包装库是个更好的选择.
系统调用细节
以 c 为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| int tun_alloc(char *dev) { struct ifreq ifr; int fd, err;
if ((fd = open("/dev/net/tun", O_RDWR)) < 0) return fd; memset(&ifr, 0, sizeof(ifr)); ifr.ifr_flags = IFF_TUN | IFF_NO_PI; strncpy(ifr.ifr_name, dev, IFNAMSIZ);
if ((err = ioctl(fd, TUNSETIFF, (void *)&ifr)) < 0) { close(fd); return err; } return fd; }
|
open
正常打开一个文件, 模式一般是 O_RDWR
可读可写.
ioctl
配置参数
- 第二个参数是命令码, 这里是
TUNSETIFF
设置 tun/tap 设备的工作模式. - 最后一个是参数,
TUNSETIFF
命令的参数是 struct ifreq
类型- ifr_name 是 tun/tap 设备名
- ifr_flags 是字符设备标志
- IFF_TUN / IFF_TAP 代表是 tun 还是 tap
- IFF_NO_PI 代表不包含附加的包头, 默认会带上额外 4 字节.
读取/写入即标准的 read 和 write;
例程
最小的 tun 设备使用程序,它使用 tun0 设备,打印收到的所有数据包,并回送它们。
C
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
| #include <fcntl.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/ioctl.h> #include <linux/if.h> #include <linux/if_tun.h>
int tun_alloc(char *dev) { struct ifreq ifr; int fd, err;
if ((fd = open("/dev/net/tun", O_RDWR)) < 0) return fd; memset(&ifr, 0, sizeof(ifr)); ifr.ifr_flags = IFF_TUN | IFF_NO_PI; strncpy(ifr.ifr_name, dev, IFNAMSIZ);
if ((err = ioctl(fd, TUNSETIFF, (void *)&ifr)) < 0) { close(fd); return err; } return fd; }
int main(int argc, char *argv[]) { char tun_name[IFNAMSIZ]; unsigned char buf[1518]; int tun_fd; int nread;
strcpy(tun_name, "tun0");
tun_fd = tun_alloc(tun_name); if (tun_fd < 0) { perror("Could not open tun interface"); exit(1); }
while (1) { nread = read(tun_fd, buf, sizeof(buf)); if (nread < 0) { close(tun_fd); perror("Reading from tun interface"); exit(1); } printf("Read %d bytes from tun interface\n", nread); nread = write(tun_fd, buf, nread); printf("Write %d bytes to tun interface\n", nread); } return 0; }
|
Rust
rust 的 tun_tap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| use std::io; use tun_tap::{Iface, Mode};
fn main() -> io::Result<()> { let mut config = tun_tap::Configuration::default(); config.name("tun0".into()) .mode(Mode::Tun) .address((10, 0, 0, 1)) .netmask((255, 255, 255, 0)) .up();
let mut nic = Iface::from_configuration(&config)?; let mut buf = [0u8; 1504];
loop { let nbytes = nic.recv(&mut buf[..])?; eprintln!("Read {} bytes: {:?}", nbytes, &buf[..nbytes]); nic.send(&buf[..nbytes])?; } }
|
上面的例子仅仅展示了 tun/tap 设备最基本的使用,实际编程中,还需要根据具体应用场景,实现复杂的网络功能。
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
| use std::ffi::CString; use std::io; use std::io::prelude::*; use std::os::unix::io::AsRawFd;
use libc;
fn main() -> io::Result<()> { let tun_name = CString::new("tun0").unwrap(); let mut buf = [0u8; 1504];
let tun_fd = unsafe { let fd = libc::open( "/dev/net/tun\0".as_ptr() as *const libc::c_char, libc::O_RDWR, ); if fd < 0 { panic!("failed to open /dev/net/tun"); }
let mut ifr: libc::ifreq = std::mem::zeroed(); ifr.ifr_ifru.ifru_flags = (libc::IFF_TUN | libc::IFF_NO_PI) as libc::c_short; std::ptr::copy_nonoverlapping( tun_name.as_ptr(), ifr.ifr_ifrn.ifrn_name.as_mut_ptr() as *mut libc::c_char, tun_name.as_bytes().len(), );
let ret = libc::ioctl(fd, libc::TUNSETIFF, &ifr as *const libc::ifreq); if ret < 0 { panic!("ioctl TUNSETIFF failed"); }
fd };
let tun = unsafe { std::fs::File::from_raw_fd(tun_fd) };
loop { let nbytes = tun.read(&mut buf)?; eprintln!("Read {} bytes: {:?}", nbytes, &buf[..nbytes]); tun.write_all(&buf[..nbytes])?; } }
|
Faq
tun 与 tap 设备的区别是什么?
tun 设备是三层设备,操作 IP 数据包;tap 是二层设备,模拟以太网设备,操作链路层帧。
tun/tap 收发的数据包包含哪些内容?
对 tun 设备,需要自行构造 IP 头部,操作系统只处理 IP 层之上的部分。对 tap 设备,需要构造完整的以太网数据帧,包括 MAC 地址等。
可以在 tun/tap 设备上运行 TCP/IP 协议栈吗?
完全可以。事实上,一个常见的做法是,在 tap 设备上运行一个用户层的轻量级 TCP/IP 协议栈,实现可定制、易调试的虚拟网络。
tun/tap 设备是否支持多队列?
Linux 内核从 3.8 版本开始,支持多队列 tun/tap 设备。通过 TUNSETQUEUE
配置项,可以请求内核创建多个收发队列。这对于高性能应用很有帮助。
除了上面的内容,对 tun/tap 编程,还有一些值得注意的地方:
- 设备何时收到数据包?如果没有数据包,读取设备文件会阻塞。所以实现一个高效的事件循环很重要。
epoll
或类似机制配合非阻塞 IO,是常用方法。 - 多线程/多进程模型: 可以利用多线程/进程,充分利用多核性能。通常一个线程用于收包,多个工作线程处理具体业务。
- 用户空间 TCP/IP 协议栈的性能优化: 实现用户空间协议栈时,要注意缓存、算法、数据结构的优化。一些性能关键点如 TCP/IP 校验和的计算等,可以用 SIMD 指令集如 AVX512 加速。
- 安全性问题:tun/tap 设备是一种特殊文件,具有较高的特权,不当使用可能带来安全隐患。比如在 VPN 场景下,用户的所有流量都会经过 tun/tap 设备,设备的访问控制和程序的安全性需要仔细设计。