原文:Patrick McCanna[1]写于 2026 年 3 月 1 日
把 Linux 系统变成网络设备,到底改了什么?
我一直觉得把 Linux 变成路由器或交换机是件很酷的事。但你有没有想过:当我们把 Linux 做成路由器、把树莓派变成 WiFi 热点时,底层到底改了哪些配置?变动有多大?要开启哪些门才能让 Linux 转发和处理数据包?
我会先用讲故事的方式过一遍这些改动,然后给出具体命令。
说实话,我有认知偏见:我总觉得网络设备和普通电脑是两回事。因为在网络设备上的命令行体验,和服务器/工作站完全不同。在服务器上,你大部分时间打交道的是文件系统里的对象;在网络设备上,你是在直接摆弄运行中的进程。命令和目标都不一样。
我猜很多搞网络的人也有类似感觉:网络设备 vs 主机操作系统。也许这只是我个人的经历。不管怎样,我以前真的觉得网络和通用计算是两码事。其实不是。如果你懂网络,只要做 7 个改动,就能让 Linux 干网络设备的活。
- 6. 用 dnsmasq 提供 DHCP 和 DNS
每个 Android 手机开启个人热点时,内核做的也是这同一套操作。
数据包在内核里的旅程
假设我们有一台 Linux 机器,上面只有一块网卡。一个数据包从外网网卡进来。网卡(NIC)发一个中断,驱动通过 DMA(直接内存访问)把帧拉到内核内存里的环形缓冲区,整个过程 CPU 不插手。内核网络栈从这里接过帧,剥掉以太网头,检查 IP 目的地址。
然后内核去查路由表。如果目的地址匹配本机某个接口,数据包就继续往上走,到达监听该端口的 socket 和进程。如果目的地址不属于本机,且 IP 转发被关闭,内核直接丢弃数据包,并在 /proc/net/snmp 里计数加一。
Linux 的默认行为就是"终点":它不会把数据包转发给另一台主机。要让它当路由器,就必须做改动。而且我们还需要第二张网卡才能把包从一个接口发到另一个接口。工作站是主机,不是路由器。
现在想象一下同一台机器有两张网卡(双网卡)——我们怎么让它开始路由数据包?
路由器的任务,就是把单网卡主机默认丢弃的那些包转发出去。下面一步步看,怎么把内核从一台保守的工作站,改造成能路由包、修改包头、过滤接口间流量的路由器。
什么是 hook?
在 Linux 内核里,hook 是代码路径上的一个拦截点,外部函数可以注册自己在这里执行。想象成流水线上的一个工位:主流程在预设点暂停,按优先级顺序执行所有注册在这个工位上的函数。每个注册函数可以检查、修改、接受或丢弃经过的对象。
Hook 让内核把核心包处理逻辑和策略决策(过滤、地址转换)解耦。内核定义 hook 的位置;管理员和 nftables 等工具决定每个 hook 上运行什么代码。内核把 hook 实现为函数指针数组,存储在类似 struct nf_hook_entries 的结构里。在每个 hook 点,内核通过 nf_hook_slow() 遍历数组,把指向数据包 sk_buff 结构的指针传给每个回调。
内核网络栈与 netfilter
数据包到达网卡,驱动把它放进内存,内核网络栈按顺序分几个阶段处理它。在这个路径的若干确定节点上,内核把包交给 netfilter——一个直接内嵌在内核网络代码里的、基于 hook 的框架。
Netfilter hook 是注册在内核包处理路径里的函数指针数组。在每个 hook 点,内核按优先级遍历所有注册函数,传入指向数据包 socket buffer(sk_buff)的指针。每个函数可以接受、丢弃、修改或排队这个数据包。用户空间工具(如 nftables)通过 netlink socket(一种专门用于网络配置的内核-用户空间 IPC 通道)发送命令,把回调函数注册到这些 hook 上。
你可以实时观察 netfilter 的活动:
- •
nft list ruleset 看当前注册的表和链 - •
perf trace 或 bpftrace 可以挂探针到 nf_hook_slow 等内核函数,实时观察每个包的决策
五个标准 hook 点:
| |
|---|
| PREROUTING | |
| INPUT | |
| FORWARD | |
| OUTPUT | |
| POSTROUTING | |
PREROUTING 之后,内核做路由决策。目标是本机的包走 INPUT;目标是其他主机的包,如果转发已开启,就走 FORWARD,然后出 POSTROUTING。每一步配置要么在某个 hook 上注册代码,要么改变路由决策的行为。
改动 1:开启 IP 转发
IP 转发是跨接口转发数据包的第一道门。没有它,FORWARD hook 虽然存在,但内核永远不会把包送过去。目的地址是外地的包,在查完路由表后就被丢弃。开启后,内核把这些包交给 FORWARD,路由器配置的其余部分才能生效。
通过 /etc/sysctl.d/10-forward.conf 管理:
net.ipv4.ip_forward=1
/etc/sysctl.d/ 是内核运行时参数的 drop-in 配置目录。启动时 systemd-sysctl.service 会读取该目录下所有 *.conf 文件(以及 /etc/sysctl.conf),把每个参数写到 /proc/sys/ 下对应路径。
/proc/sys/ 是一个虚拟文件系统,每个可调参数都表现为一个文件。net.ipv4.ip_forward 对应 /proc/sys/net/ipv4/ip_forward。写 1 就是告诉 IPv4 协议栈:遇到目的地址非本地的包时,送进 FORWARD hook 而不是丢弃。内核在 net/ipv4/ip_forward.c 的 ip_forward() 函数里实现这个决策。
写进 sysctl.d/10-forward.conf 可以确保重启后仍然生效。重启 systemd-sysctl.service 可以立即生效,不用重启系统。随时可以用 cat /proc/sys/net/ipv4/ip_forward 验证:1 表示转发开启,0 表示关闭——此时哪怕其他配置都配好了,路由器也不会工作。
第一个改动:把 ip_forward 设为 1。
改动 2:定义网桥:把两个接口collapse成一个网段
家庭网络里有线和无线客户端都在同一个子网。配置就是建一个网桥 br0,然后把 eth0 和 wlan0 作为成员端口绑上去。
第二个改动:定义网桥,并加入接口,让它们能在二层互通。
网桥工作在二层(以太网层)。内核的 bridge 模块维护一张 MAC 地址转发表。帧从 eth0 进来时,网桥查目的 MAC,转发到该地址上次出现的端口;如果未知,就泛洪到所有成员端口。 learn 到的关联会在可配置的老化时间后过期。对外来说,br0 就像一个统一的交换机,有线和无线接口共享同一个二层网段。内核在 net/bridge/br_forward.c 的 br_forward() 里实现桥接转发逻辑。
这对路由很重要,因为内核把 IP 地址分配给接口,而不是物理端口。把 192.168.1.1 配给 br0,意味着不管客户端是有线还是无线,路由器只持有一个 LAN 地址。两个接口在同一个子网内转发流量,互相之间不需要路由决策。
有个重要区别:有线接口如 eth0 可以用一条命令直接奴役给网桥(ip link set eth0 master br0),内核 bridge 模块立刻开始从上面 learn MAC 地址。无线接口(wlan0)不能这么干。
802.11 协议要求关联和认证生命周期,标准以太网桥接管不了这些。所以由 hostapd 处理:hostapd.conf 里的 bridge=br0 指令告诉 hostapd,等接口进入 AP 模式后把 wlan0 接到网桥上。无线客户端关联成功后,对网桥来说就像接在了一个有线端口上。最终逻辑效果一样:统一的 L2 网段,但有线和无线成员走的路径不同。
按 kernel hostapd 文档[2]的说法:
mac80211 子系统把 master 模式的方方面面都移到了用户空间。它依赖 hostapd 处理客户端认证、设置加密密钥、建立密钥轮换策略等无线基础设施的方方面面。因此,以前那种 iwconfig <wireless interface> mode master 的方式已经不再有效。
在标准以太网桥端口上,任何发帧的设备都会被 learn MAC——二层没有预先握手。但在 802.11 AP 上,MAC 层本身就要求客户端先完成认证和关联(State 3),AP 才会接受或转发它的数据帧。AP 的 MAC(由驱动通过 mac80211 管理)是守门人,它需要用户空间守护进程(hostapd)来处理认证交换。内核的 bridge 模块对 802.11 状态一无所知——它只看到帧——所以它没法自己管理这个生命周期。
bridge-utils 包提供 brctl 用于查看网桥状态。内核通过 br_netfilter 和 bridge 模块处理所有转发逻辑。
题外话:网桥与抓包。 网桥端口很适合插抓包。往 br0 上接一个 tap 设备做流量镜像(tap/tun 虚拟接口详见 kernel tuntap 文档[3]),或者用一个 promiscuous 模式的端口把流量喂给 tcpdump 或 Zeek。因为网桥在路由或过滤决策之前就看到了网段上的所有帧,在这里抓包能看到完整的、NAT 前、防火墙前的流量全貌。tcpdump -i br0 或绑定到桥接口的 AF_PACKET socket 对大多数家庭和小型办公流量都能跑满线速。在默认 Linux 内核上,这些工具大约能跑到 18 Gbps(我上次测试大概在 2023 年左右)。更高的线速需要基于硬件过滤的方案,比如 DPDK 或 XDP。
改动 3:启用 nftables 策略:在 hook 上安装代码
有了网桥之后,我们需要通过 netfilter 的 nftables 定义包处理规则。
Netfilter 是更广义的、在内核层面提供包过滤框架和 hook 的机制;nftables(通过 nf_tables)则是运行在这些 hook 之上的现代包分类引擎,替代了 iptables,但两者最终依赖的是内核里同一套 netfilter hook 基础设施。内核在 net/netfilter/nf_tables_api.c 里实现 nf_tables 子系统。
/etc/nftables.conf 里的防火墙和 NAT 规则本质上就是回调注册。nftables 通过 netlink socket 把它们发给内核,nf_tables 子系统再把这些规则装到指定的 hook 上。每条链声明都显式指明了 hook 和优先级:
chain forward { type filter hook forward priority 0; policy drop; iifname "eth0" oifname "br0" ct state { established,related } counter accept iifname "br0" oifname "eth0" ct state { new,established,related } counter accept counter}
这条链控制接口间的转发流量,是路由器的核心工作。拆解一下:
链定义
type filter hook forward priority 0; policy drop;
这挂在了 netfilter 的 forward hook 上,也就是说它只处理那些不是发给本机、但需要穿过去的包。默认策略是 drop,没被显式允许的包都会被静默丢弃——这是 deny-by-default 的姿态。
在这个 WiFi AP 场景里,eth0 是 WAN 口,连向 ISP 或上级路由器;br0 是 LAN 口桥,聚合了有线客户端(如果有直接接的话)和 hostapd 管理的无线客户端。所有 LAN 流量都通过 br0 进出,不管它来自有线还是无线设备。
规则 1:WAN → LAN(只允许回程流量)
iifname "eth0" oifname "br0" ct state { established,related } counter accept
从 WAN(互联网侧)进来、要去 LAN 的流量,只有 conntrack 显示这条连接是由 LAN 侧先发起的情况下才被接受。这意味着来自互联网的主动入站连接被阻断,这正是 NAT 路由器/防火墙应有的行为。
规则 2:LAN → WAN( outbound 流量)
iifname "br0" oifname "eth0" ct state { new,established,related } counter accept
从 LAN 出去、要去 WAN 的流量,无论是新建连接还是已有连接,都被允许。这让 LAN 客户端可以自由访问互联网。
末尾的 counter
这是一个不带动作的兜底计数器,它统计那些没匹配上前面任何规则的包(这些包最终会被策略 drop 掉),用于监控被拒绝的流量。
这是典型的"有状态防火墙"模式:LAN 设备可以主动连出去,但互联网不能主动连进来。related 状态还允许 ICMP 错误、FTP 数据通道等与已有连接关联的次生流量通过,不用为每种协议变体写显式规则。
nftables.service 加载或重载配置时,会原子式地 flush 现有规则集并通过 netlink 安装新规则。过渡期间不会有数据包看到残缺的规则集。重载命令:
sudo systemctl reload nftables.service
验证配置是否正确:
sudo nft -c -f /etc/nftables.conf
如果你想深入 netfilter,Oracle 的 这篇博客[4] 非常棒。
第三个改动:定义 nf_tables 规则来处理数据包。
改动 4:用 conntrack 做有状态防火墙
规则片段里的 ct state { established, related } 和 ct state { new, established, related } 引用了 conntrack——内核的连接跟踪子系统。Conntrack 让两条简单规则足以处理所有合法流量。内核在 net/netfilter/nf_conntrack_core.c 里实现连接跟踪核心。
Conntrack 在数据包经过 netfilter 时观察流量,维护一张活跃连接表。每条记录保存源/目的地址、端口、协议和当前连接状态。当 LAN 客户端向互联网上的服务器发起 TCP 连接时,conntrack 创建一条记录,标记为 new。三路握手完成后,标记为 established。互联网来的回复包在 FORWARD 链里匹配 ct state established,自动放行。
防火墙允许 br0 到 eth0、携带 new 或 established 状态的 outbound 连接。回程包在 eth0 上匹配 established。Conntrack 负责记账,防火墙规则只负责查表。
related 状态覆盖次生流量。比如 FTP 先开一个控制连接,再协商一个独立的 port 做数据传输;ICMP 错误消息关联到现有 TCP/UDP 流。Conntrack 理解这些关系,把它们标记为 related,防火墙无需为每种协议的变体写显式规则就能放行。
第四个改动:扩展内核连接跟踪子系统,开始跟踪超越本主机的网络连接。
改动 5:定义 NAT 和 Masquerade 策略:在边界改写地址
家庭网络使用 RFC 1918 私有地址空间:10.0.0.0/8、172.16.0.0/12、192.168.0.0/16。公共互联网不路由这些网段。每个离开 LAN 的数据包,都需要把源地址替换成路由器的公网 IP,否则发出去也收不到回复。
postrouting 链在 POSTROUTING hook 上把 outbound 包的私有源地址换成路由器的公网地址:
chain postrouting { type nat hook postrouting priority 100; policy accept; oifname "eth0" counter masquerade}
Masquerade 这个词本身就带有"伪装"之意。路由器假装自己是发往互联网的请求的原始发送者,但它记得内网里到底是哪个节点发起的请求。互联网上的资源回复给路由器,路由器再修改包并转发给真正的请求者。LAN 客户端对外呈现的是路由器的 WAN IP,原始私有地址被隐藏在公网地址后面。内核在 net/netfilter/nf_nat_masquerade.c 里实现 masquerade 动作。
Conntrack 把这次地址转换作为流记录的一部分保存下来。(private IP, private port, public IP, public port, protocol) 这个五元组在连接存活期间一直留在 conntrack 表里。你可以直接查看:
sudo conntrack -L
每行显示一条活跃流的原始方向和回复方向五元组,以及连接状态和超时倒计时。空闲时间足够长的流会被老化删除,这是防止 NAT 表无限增长的关键机制。TCP 连接在会话关闭或可配置的空闲期后超时;UDP 用更短的定时器,因为 UDP 没有关闭信号。
masquerade 动作在包被处理的那一刻读取 eth0 的当前 IP 地址,而不是在配置时固定死。所以如果 WAN 口通过 DHCP 获取地址,公网 IP 变了也不用担心——新连接会自动使用新地址。已建立连接的老地址由 conntrack 保留到它们过期为止。
第五个改动:定义规则来修改经过本主机的数据包中的发送方和接收方地址。
改动 6:用 dnsmasq 提供 DHCP 和 DNS:向新客户端宣告路由器存在
每台连上网的电脑都需要三件事:自己的 IP 地址、默认网关、DNS 服务器。
路由器必须向网络里的客户端自我介绍。新设备加入时没有 IP、没有网关、没有 DNS 解析器。dnsmasq[5] 通过 DHCP 把这些信息发给客户端。
设备入网时会广播 DHCP discover。dnsmasq 在 br0 上监听并响应一个 offer,包含 IP 地址、子网掩码、租约时长,以及两个 DHCP option:option 3(默认网关,192.168.1.1)和 option 6(DNS 服务器,192.168.1.1)。Option 3 告诉客户端非本地子网的目的地该往哪发;Option 6 告诉客户端该问哪个解析器。dnsmasq 还会缓存上游 DNS 的响应,减少查询量并加速重复解析。
dnsmasq 只绑定在 br0 上,所以它只服务 LAN,永远不会在 eth0 上监听。
NetworkManager 作为替代方案:NetworkManager[6] 也能通过内置的 dnsmasq 集成来处理 DHCP 和 DNS,只需在 /etc/NetworkManager/NetworkManager.conf 里设置 dns=dnsmasq。NetworkManager 会启动自己的 dnsmasq 实例,并随接口插拔动态管理配置。
两种方式各有取舍。NetworkManager 减少了手动配置,能自动处理接口生命周期事件,适合笔记本或接口经常变化的机器。但在 dedicated 路由器上,你通常想要更强的控制力。NetworkManager 可能会在网络事件触发时重新配置或重启 dnsmasq,以不可预测的方式打断 DHCP 租约。而用 systemd 启动的静态 dnsmasq 配置,能给你确定的启动顺序、显式的绑定地址,以及通过 journalctl -eu dnsmasq.service 一目了然的日志。你知道守护进程在干什么,因为配置文件是你亲手写的。
从内核视角看,两条路最终到达同一个地方:一个绑定在 UDP 67 端口上的用户空间进程,在网桥接口上响应 DHCP 请求。内核并不区分这两种安排。差异在于守护进程如何启动、配置和监督。这是服务管理和运维层面的取舍,不是架构层面的。
第六个改动:部署 dnsmasq 守护进程,为系统网络上的客户端提供 DHCP 和 DNS 服务。
改动 7:用 hostapd 提供 WiFi 热点:把无线网卡切到 AP 模式
无线网卡有多种工作模式。Managed 模式下,网卡扫描 AP 并作为客户端去关联。AP 模式下,网卡广播 beacon、接收关联请求、管理连接设备的完整认证生命周期。
内核的 mac80211[7] 子系统为不同驱动的 802.11 硬件提供了统一的编程接口。hostapd[8] 通过 nl80211[9] netlink 接口与 mac80211 通信——和 nftables 用的是同一种基于 socket 的内核-用户空间通道,只是这里应用于无线子系统。hostapd 通过 nl80211 命令驱动进入 AP 模式,设置 SSID、信道、WPA2 加密参数,并接管认证帧。
hostapd.conf 里的 bridge=br0 指令把 AP 接口作为成员端口接到网桥上。无线客户端关联成功后,进入和有线客户端相同的二层网段。它们的流量到达 br0,内核执行同样的 netfilter 决策,数据包走和 LAN 上其他设备完全一样的转发路径。
Debian 默认把 hostapd 服务 mask 掉。systemd 注册了该服务但阻止它启动,这是为了防止未配置的实例直接启动并广播一个开放网络。systemctl unmask hostapd 解除这个封锁,然后 systemctl enable --now hostapd 立即启动并设为开机自启。
第七个改动:部署 hostapd 守护进程,让设备的 WiFi 网卡对外提供无线网络。
结果:一台 WiFi 路由器
每个配置步骤都激活了内核网络架构的不同层次。合起来,它们构成了一个完整的转发系统:
网桥行的说明:把有线接口加进 br0 是直接的内核操作——bridge 模块立刻接管该端口的帧转发。把无线接口加进去则是间接的:hostapd 的 bridge=br0 指令在无线网卡进入 AP 模式、客户端关联之后才处理接入。两者最终逻辑效果相同:统一的 L2 网段,但机制不同。如果你调试桥接成员关系,brctl show(或 ip link show master br0)会直接显示有线成员;无线客户端关联后作为 learn 到的 MAC 出现在桥接转发表里,可以用 brctl showmacs br0 查看。
从最初的 Linux 机器开始:默认状态下它是一台工作站——只接收给自己的包,不转发任何流量,丢弃不属于自己 IP 的流量。IP 转发大门紧闭,netfilter FORWARD 链空空如也,无线网卡在监听 beacon 而不是广播 beacon,没有 DHCP 服务器,没有 NAT 表,没有网桥。
- • 网桥把有线和无线接口 collapse 成一个可寻址域。
- • nftables 链在 FORWARD hook 上安装策略,决定什么能通过、什么被丢弃。
- • Conntrack 为策略决策提供状态信息,让简单规则能应对复杂流量模式。
- • Masquerade 把 LAN 藏在路由器的公网身份后面,同时在内存中维护转换表。
- • dnsmasq 宣告路由器的存在,给每个新客户端发放访问外网所需的信息。
- • hostapd 把一张客户端模式的射频卡变成了接入点。
就是这些改动,把一台 Linux 系统变成了 WiFi 路由器。你可以用 6 条命令来检查和观察它们:
- •
cat /proc/sys/net/ipv4/ip_forward —— 查看转发状态 - •
nft list ruleset —— 查看活跃防火墙策略 - •
conntrack -L —— 查看实时流和 NAT 映射 - •
journalctl -eu dnsmasq.service —— 查看 DHCP 租约活动
引用链接
[1] Patrick McCanna: https://patrickmccanna.net/7-configuration-changes-that-turn-a-multi-homed-host-into-a-switch-router/[2] kernel hostapd 文档: https://wireless.docs.kernel.org/en/latest/en/users/documentation/hostapd.html[3] kernel tuntap 文档: https://www.kernel.org/doc/html/latest/networking/tuntap.html[4] 这篇博客: https://blogs.oracle.com/linux/introduction-to-netfilter[5] dnsmasq: https://thekelleys.org.uk/dnsmasq/doc.html[6] NetworkManager: https://networkmanager.dev/[7] mac80211: https://elixir.bootlin.com/linux/v6.8/source/net/mac80211/[8] hostapd: https://w1.fi/hostapd/[9] nl80211: https://wireless.wiki.kernel.org/en/developers/documentation/nl80211