运维笔记:NetworkManager shared 模式给共享客户端固定 IP

Manjaro Linux 用 WiFi(wlp7s0)上网,把网线口(enp8s0)共享给一台下游设备。共享用的不是独立 dnsmasq,而是 NetworkManager 自带的 shared 模式。下游设备能正常上网,但 IP 是 DHCP 动态分的,今天 .108,下次可能 .109。想把那台设备固定成 10.42.0.2,方便后面做端口转发和固定访问。

现场

笔记本同时连着两张网卡:

1
2
3
4
5
6
7
8
9
10
11
12
13
              上游 WiFi(出口)
wlp7s0 192.168.1.x/24 gw 192.168.1.1 (示例网段)

┌────┴─────┐
│ 笔记本 │ Manjaro 26.1 / Kernel 7.1.4
│ │ NetworkManager 1.58(nmcli)/ dnsmasq 2.93
│ │ ipv4.method=shared(共享连接)
└────┬─────┘
│ enp8s0 10.42.0.1/24
│ └─ NM 内置 dnsmasq:DHCP 池 10.42.0.10–254,租约 3600s

下游设备(share client)
MAC aa:bb:cc:dd:ee:01 (示例) 当前拿到 10.42.0.108

NM 用 shared 模式把 enp8s0 当 LAN 口,自动开了:

  • 本机 enp8s0 = 10.42.0.1/24(shared 模式固定用 .1,改不了)
  • 一个内置 dnsmasq 提供 DHCP,动态池 10.42.0.10 – 10.42.0.254
  • net.ipv4.ip_forward=1 + 自动加 iptables NAT,把流量从 enp8s0 转到 wlp7s0 上网

确认连接是 shared 模式(下面命令里的 <共享连接名> 换成你自己的,比如我的叫 ETH_SHARE):

1
2
$ nmcli connection show ETH_SHARE | grep ipv4.method
ipv4.method: shared

下游那台设备(MAC aa:bb:cc:dd:ee:01,示例)当前租约:

1
2
$ sudo cat /var/lib/NetworkManager/dnsmasq-enp8s0.leases
1700000000 aa:bb:cc:dd:ee:01 10.42.0.108 mydevice 01:aa:bb:cc:dd:ee:01

定位:DHCP 实际由谁发出

shared 模式的 DHCP 不是系统级的 dnsmasq.service 发的——它是 inactive 的:

1
2
$ systemctl status dnsmasq
○ dnsmasq.service ... ; Active: inactive (dead)

真正发 DHCP 的是 NM 拉起的另一个 dnsmasq 实例,看进程就能定位到它和它的配置入口:

1
2
3
4
5
6
7
8
$ ps aux | grep dnsmasq | grep -v grep
nobody 1234 /usr/bin/dnsmasq --conf-file=/dev/null --no-hosts --keep-in-foreground \
--bind-interfaces --except-interface=lo --clear-on-reload --strict-order \
--listen-address=10.42.0.1 \
--dhcp-range=10.42.0.10,10.42.0.254,3600 \
--dhcp-leasefile=/var/lib/NetworkManager/dnsmasq-enp8s0.leases \
--pid-file=/run/nm-dnsmasq-enp8s0.pid \
--conf-dir=/etc/NetworkManager/dnsmasq-shared.d

这条启动参数里有三个关键点:

  • --conf-file=/dev/null —— 这个实例显式忽略 /etc/dnsmasq.conf,所以 DHCP 的定制入口不在那里。
  • --conf-dir=/etc/NetworkManager/dnsmasq-shared.d —— 真正的配置目录,NM 给 shared 模式专用,默认是空目录。这就是塞 dhcp-host 的地方。
  • 它的生命周期挂在那个共享连接上——NM 激活连接时拉起、停用时杀掉,不归 dnsmasq.service 管。所以重载配置要靠重连,而不是 systemctl restart dnsmasq

保持 NM 全托管即可,不要去启用 dnsmasq.service。它和这个内置实例会抢 53/67 端口,造成 DHCP 双重应答、DNS 冲突。

修复:往 conf-dir 放 dhcp-host 静态绑定

目标:把下游设备(MAC aa:bb:cc:dd:ee:01,示例)固定成 10.42.0.2

1
2
3
4
5
6
7
$ sudo mkdir -p /etc/NetworkManager/dnsmasq-shared.d

$ sudo tee /etc/NetworkManager/dnsmasq-shared.d/static-leases.conf >/dev/null <<'EOF'
# 共享连接 (enp8s0) 客户端固定 IP 绑定
# 10.42.0.2 在默认动态池 10.42.0.10–254 之外,dnsmasq 不会动态发出,无冲突
dhcp-host=aa:bb:cc:dd:ee:01,10.42.0.2,mydevice,infinite
EOF

dhcp-host 的格式:<MAC>,<IP>,<hostname>[,infinite]infinite 是永久租约,IP 永远属于这台设备。

写完先做语法自检,别等重连了才发现 dnsmasq 起不来:

1
2
$ sudo dnsmasq --test --conf-file=/dev/null --conf-dir=/etc/NetworkManager/dnsmasq-shared.d
dnsmasq: syntax check OK.

然后让 NM 重启它的 dnsmasq。最干净的方式是重连那个共享连接(下文记作 ETH_SHARE,换成你自己的连接名),NM 会自动杀掉旧 dnsmasq、起新的并加载 conf-dir:

1
$ sudo nmcli connection down ETH_SHARE && sudo nmcli connection up ETH_SHARE

为什么不直接 kill -HUP 那个 dnsmasq?也能用,sudo kill -SIGHUP $(cat /run/nm-dnsmasq-enp8s0.pid) 会让它重读配置、不中断现有连接。但重连更彻底——尤其当配置改了 DHCP 池范围时,HUP 不一定全量重载。这次只加 dhcp-host,两者都行,我选了重连。

验证新 dnsmasq 起来了、且带上了 conf-dir:

1
2
3
4
5
$ cat /run/nm-dnsmasq-enp8s0.pid
5678 # PID 变了(重启前是另一个值),确认实例重启

$ ps -o args= -p $(cat /run/nm-dnsmasq-enp8s0.pid) | tr ' ' '\n' | grep conf-dir
--conf-dir=/etc/NetworkManager/dnsmasq-shared.d

到这一步服务端就配完了。

客户端必须重拿一次 DHCP

配完绑定,下游设备的 IP 不会自动从 .108 变成 .2静态绑定只在客户端下次续约/重连时生效——这是最容易漏的一步。

  • Linux 客户端:sudo dhclient -r && sudo dhclient
  • 嵌入式/摄像头/手机:拔插网线或重启设备即可

重连后回笔记本看租约:

1
2
3
4
5
6
$ sudo cat /var/lib/NetworkManager/dnsmasq-enp8s0.leases
# 期望:aa:bb:cc:dd:ee:01 10.42.0.2 mydevice ...

$ ping -c2 10.42.0.2
$ ip neigh show dev enp8s0
# 期望:10.42.0.2 lladdr aa:bb:cc:dd:ee:01 REACHABLE

小结

NetworkManager 的 shared 模式是个黑盒:表面上是 NM 一个连接配置,背后它替你拉起了一整套 dnsmasq + iptables + ip_forward。好处是开箱即用,代价是 DHCP 的定制入口藏得很深——不是 /etc/dnsmasq.conf,而是 /etc/NetworkManager/dnsmasq-shared.d/

记住这个映射关系就够了:改 shared 模式的 DHCP 行为,找 NM 内置 dnsmasq 的 conf-dir,别找系统的 dnsmasq.service。