把 tun0 和某种 VPN 协议当成同一件事,会让人去翻错日志。接口是一道本地边界,用户态程序就在这里与内核交换 IP 包。协议则是程序拿到包之后所做的事:传输、认证、加密,以及一个对端。创建设备并不选择其中任何一项。

两个组件,一条包路径

内核把 IP 包写进设备,程序再从文件描述符里把它读出来。反方向上,程序把包写回去,内核就像它是从真实接口抵达那样进行路由。TUN 承载 IP,而 TAP 承载以太网帧。在这道边界上读一个包就能看到分帧方式,而设备正是为此而存在。

边界之外的一切都是程序的事。两个程序可以使用同一个接口概念,却对包做出完全不同的处理。设备本身对接下来的事没有任何主张。文件描述符是程序在内核一侧唯一的把手。

先创建设备,再观察边界

测试边界时要刻意创建一台设备。持久设备在创建它的进程退出后仍然存在,这对实验很有用,之后也常常成为混乱的来源。先检查标志和属主,再去怪 VPN。

sudo ip tuntap add dev tun9 mode tun user alice
ip -d link show tun9

链路详情会显示设备类型和挂上的属主。如果没有进程打开它,写进去的包哪里也去不了。没有进程挂接的设备,在链路上看起来可能很健康,却什么也不转发。

接口名保证不了什么

接口名对加密或认证只字未提。程序可以把包封装进 TLS、自有协议、IPsec,也可以什么都不封。选 TUN 而不是 TAP,改变的是边界上的分帧,而不是安全设计。分帧的选择是真实的,但它比安全问题小得多。

检测也是同样的道理:观察者看到的是线路上的外层协议,而不是一块标签说明是哪个本地设备产生的。一条看起来像别的东西的隧道,是因为它的外层协议才如此,不是因为接口名。观察者看不到是哪个本地设备产生了包,只能看到包长什么样。比较产品时要把这两个问题分开,因为一个的答案永远推不出另一个。

让修复对上出错的那一段

出问题时要把故障阶段拆开。设备缺失、权限错误和被拒绝的登录会产生不同的证据、不同的修法。先读本地状态,再读程序自身的日志,最后读对端的视角。三样都读完再动手改。

ip -d link show tun0
journalctl -u openvpn-client@corp --since '10 min ago'

如果设备存在且有进程持有它,本地边界就是通的,问题在更外面。如果设备不存在,再怎么核对凭据也没用。能叫得出名字的阶段,才是能测试的阶段。TUN/TAP 文档描述了这道边界和设备模型。

相关文章