树莓派插上网线就能连:给 eth0 加 link-local 回退
eth0 只配了 dhcp4 时,遇到不发地址的对端就彻底连不上。加一份 netplan 覆盖文件:DHCP 优先,失败约 10 秒后自动补 169.254,另留一个永久锚点。文末是两个幂等脚本。
起因
朋友给了一台树莓派。我先把电脑开成一个同名热点,想让 Pi 自己连上来,没成。改用网线直连,还是有问题。于是有了这套调研。
这台树莓派 5(Ubuntu 24.04)原来只有一个 `dhcp4: true`:谁来发地址,谁就是单点故障。
中间接个交换机、或者换成一台默认不发地址的电脑直连,就拿不到 IPv4,只剩 mDNS 和 IPv6 那两条路可走。
现在的目标是插上网线就能连:
```bashssh pi # 等价于 ssh pi@pi.local
```
下面把主机名与用户名都写成 `
pi`,命令里换成你自己的即可。
先决条件
- 一台跑 Debian / Ubuntu 的单板机(本次是树莓派 5),网络栈是 netplan → systemd-networkd
- 有 `
sudo`:要往 `/etc/netplan/` 写文件,再跑一次 `netplan apply` - 客户端这边不用装什么:Linux 上 `
.local` 要有 mDNS(`libnss-mdns` 与 `avahi-daemon`),装不了就走固定锚点 - 想用锚点地址的话,客户端要能给自己加一个 `
192.168.77.2/24`
这套做法在什么环境上验证过
```plaintext树莓派 5 | Ubuntu 24.04 | systemd 255(netplan → systemd-networkd) 客户端:CachyOS(Arch)—— 下面所有通路都是在这一台机器上测的
```
只在这一个组合上实测过。 下面这些地方会因环境而异:
- 「DHCP 失败才补 IPv4LL」的语义:✅ systemd-networkd 通用,出处见第三节的 man 引文。
- 覆盖文件的加载顺序:✅ netplan 按文件名字典序合并,`
70-` 排在 `50-` 之后,所以它是补充而不是被顶掉。 - `
link-local: [ipv4, ipv6]` 这个键:✅ netplan 通用。不用 netplan 的话,对应的 systemd-networkd 设置是 `LinkLocalAddressing=`。 - avahi 发布的 `
名字.local`:✅ 通用,跟着主机名走,与地址无关。 - IPv6 link-local 与 MAC 绑定:✅ 通用,换网卡或换机器就会变。
- Windows / macOS 直连:⚠️ 没有实测。按它们的默认行为写(Win/mac 会自己配 IPv4LL、macOS 自带 Bonjour),照抄前自己确认一遍。
- 手机 + USB-C 网卡走锚点:⚠️ 没有实测,同上。
- 锚点地址 `
192.168.77.1/24`:⚠️ 网段是自选的,只要不和家里网段撞就行。
一、以前与现在的差别
- 电脑开着网络共享:以前能连(NetworkManager 的共享模式会发 `
10.42.0.x`),现在一样能连。 - 换一根网线:以前能连,现在能连(千兆口自动 MDIX,什么线都行)。
- 接交换机,没有人发地址:以前拿不到 IPv4,只能靠 mDNS/IPv6;现在约 10 秒后自动上 `
169.254.x.x`,`ssh pi` 直接可用。 - 插到家里的路由器上:以前能连,现在一样能连。
- 换一台 Windows / macOS 电脑直连:以前那台电脑默认不发地址;现在它自己会配 `
169.254.x.x`(Win/mac 的默认行为),mDNS 也能解析 `pi.local`。 - 手机加 USB-C 网卡直连:以前不行;现在 mDNS 支持差的设备可以用锚点 `
192.168.77.1`(手机静态设 `192.168.77.2/24`)。
改动只有一件:给 eth0 加了 IPv4 link-local 回退,外加一个永久的固定锚点。
二、现在同时具备的四条通路
- DHCP:对端有 DHCP 服务器时自动获得。不需要对端额外配置。
- 固定锚点 `
192.168.77.1`:永远都在。需要客户端手工配 `192.168.77.2/24`,不用配网关。 - IPv4 link-local `
169.254.x.y`:DHCP 失败约 10 秒后自动加上,地址每次自动选、会变。需要客户端也支持 IPv4LL(Win / macOS / Linux 默认都支持)。 - IPv6 link-local `
fe80::…`:与网卡 MAC 绑定,永远都在、零配置。需要客户端有 IPv6,默认都有。
名字那条路是 `pi.local`(avahi / mDNS),它会同时发布上面所有地址,地址变了自动跟着变。所以平时只需要记住 `ssh pi`。
提示
第四条通路的地址由 MAC 派生,换网卡或换硬件就变;`
pi.local` 不受影响。 网络里如果出现第二台同名机器,avahi 会把它改名成 `pi-2.local`。
三、Pi 上改了什么
新增的文件只有一个,`/etc/netplan/70-eth0-fallback.yaml`:
```yaml# 让 eth0 在有/没有 DHCP 服务器时都能连 # 优先级:DHCP(对端有服务器时,比如电脑共享或家里路由器)> 固定锚点 192.168.77.1 > IPv4 link-local 169.254.x.x network: version: 2 ethernets: eth0: dhcp4: true optional: true # 同时开启 IPv4/IPv6 链路本地地址,完全不依赖任何服务器 link-local: [ipv4, ipv6] # 固定锚点地址:给不支持 mDNS 的客户端手工直连用 addresses: - 192.168.77.1/24
```
原有的 `/etc/netplan/50-cloud-init.yaml` 没动,改之前备份了一份到 `/root/`。
应用方式:
```bashsudo netplan generate && sudo netplan apply
```
注意这套网络栈是 netplan → systemd-networkd,eth0 在 NetworkManager 里是 `unmanaged`,所以用 `nmcli` 改是没有用的。
为什么 DHCP 正常时看不到 169.254
systemd 255 的 man 页写得很直接:
An IPv4 link-local address is configured when `
yes` or `ipv4` and when DHCPv4 autoconfiguration has been unsuccessful for some time.
有 DHCP 就优先用 DHCP,失败才补 IPv4LL。这正是想要的语义:多一条兜底,不改动正常情况下的行为。
实测(用一对 veth 造了一条没有 DHCP 服务器的链路,不碰 eth0):
- `
DHCP=ipv4` 加 `LinkLocalAddressing=ipv4`:10 秒内拿到 `169.254.112.168/16`。 - 在此基础上再加静态地址(模拟锚点):两者共存,锚点不会顶掉回退。
四、两个脚本
上面那份配置手写也就十行,但「备份 → 写入 → 校验 → 应用 → 验证」这套动作值得脚本化,另外脚本里加了一道自动回滚保险。两个脚本都是幂等的,重复跑安全。
`setup-pi.sh`:在 Pi 上跑
它会先把 `/etc/netplan/` 里现有的配置备份到 `/root/`(带时间戳,不覆盖旧备份),再写新文件、`netplan generate` 校验语法,然后挂一个 180 秒的自动回滚定时器才应用 —— 如果新配置把网络弄断了,180 秒后它会自己删掉新文件并恢复。
```bash# 在 Pi 上(或 ssh 进去) ssh pi@pi.local 'sudo bash -s' < setup-pi.sh
```
```bash#!/usr/bin/env bash # 在树莓派上运行(或 ssh 进去运行):让 eth0 在没有 DHCP 服务器时也能连上。 # # 用法: ssh pi@pi.local 'sudo bash -s' < setup-pi.sh # 幂等:重复运行安全;自带 180 秒自动回滚保险。 # # 目的:不再依赖"某一台电脑负责发地址"。插上网线后,无论对端是 # - 家里路由器(有 DHCP) -> 拿路由器分配的地址 # - 一台开着网络共享的电脑 -> 拿电脑分配的地址 # - 交换机 / 另一台什么都没配的电脑 -> 自动上 169.254.x.x(IPv4 link-local) # - 永远都在 -> 固定锚点 192.168.77.1 + IPv6 链路本地 fe80:: # 四种情况都能连。 set -euo pipefail CONF=/etc/netplan/70-eth0-fallback.yaml ANCHOR=192.168.77.1/24 STAMP=$(date +%F-%H%M%S) [[ $EUID -eq 0 ]] || { echo "请用 sudo 运行:sudo bash $0" >&2; exit 1; } echo "==> 1/5 备份现有 netplan 到 /root/" for f in /etc/netplan/*.yaml; do [[ $f == "$CONF" ]] && continue dest="/root/$(basename "$f").bak-$STAMP" [[ -e $dest ]] || cp -a "$f" "$dest" # 不覆盖已有备份 done ls -1t /root/*.bak-* 2>/dev/null | head -5 echo "==> 2/5 写入 $CONF" cat > "$CONF" <<'YAML' # 由 setup-pi.sh 生成:让 eth0 在有/没有 DHCP 服务器时都能连 # 优先级:DHCP > 固定锚点 192.168.77.1 > IPv4 link-local 169.254.x.x network: version: 2 ethernets: eth0: dhcp4: true optional: true link-local: [ipv4, ipv6] addresses: - 192.168.77.1/24 YAML chmod 600 "$CONF" echo "==> 3/5 校验语法(失败则不动任何东西)" netplan generate echo " 语法 OK" echo "==> 4/5 挂 180 秒自动回滚保险,然后应用" systemd-run --on-active=180 --unit=netplan-rollback \ /bin/bash -c "rm -f $CONF; netplan apply" >/dev/null 2>&1 || true netplan apply sleep 8 echo "==> 5/5 验证" ip -brief addr show eth0 || true if ip -4 addr show eth0 | grep -q "${ANCHOR%/*}"; then systemctl stop netplan-rollback.timer 2>/dev/null || true systemctl reset-failed netplan-rollback.timer netplan-rollback.service 2>/dev/null || true echo echo "✅ 成功:锚点 ${ANCHOR%/*} 已生效,回滚保险已取消。" echo " 现在这些通路都可用(mDNS 名称 pi.local 会同时发布它们):" echo " - DHCP 地址 :有 DHCP 服务器时自动获得" echo " - 固定锚点 :${ANCHOR%/*} (客户端配 192.168.77.2/24 即可直连)" echo " - IPv4 link-local :169.254.x.x (无 DHCP 服务器时约 10 秒后自动获得)" echo " - IPv6 link-local :fe80::... (一直都在,无需任何服务器)" exit 0 else echo "⚠️ 没看到锚点地址,180 秒后会按原配置自动回滚。" >&2 exit 1 fi
```
`setup-client.sh`:在要连 Pi 的那台电脑上跑
不需要 `sudo`,不改系统配置,只往 `~/.ssh/config` 写两个别名(`pi` 走 mDNS、`pi-anchor` 走固定锚点),然后按优先级探测一遍,告诉你哪条通路通了。
```bashbash setup-client.sh
```
⚠️ 跑之前先把脚本开头那两行改成你自己的主机名与用户名(`PI_NAME` / `PI_USER`,示例值是 `pi.local` 与 `pi`)。
```bash#!/usr/bin/env bash # 在一台新电脑上运行(Linux / macOS),配好连接树莓派所需的一切。 # # 用法: bash setup-client.sh # 不需要 sudo,不改系统配置,只写 ~/.ssh/config。 # # ⚠️ 先改下面两行:主机名与用户名换成你自己的(下面是示例值)。 # # 做的事: # 1. 检查本机 mDNS(.local 域名)解析能力 # 2. 没有 SSH 密钥就生成一个 # 3. 往 ~/.ssh/config 写 pi / pi-anchor 两个别名 # 4. 按优先级探测树莓派,并说清哪条通路通了、哪条没通 set -uo pipefail PI_NAME="pi.local" PI_USER="pi" PI_ANCHOR="192.168.77.1" PI_ANCHOR_NET="192.168.77" ANCHOR_CLIENT_IP="192.168.77.2" KEY="$HOME/.ssh/id_ed25519" CFG="$HOME/.ssh/config" ok() { printf ' \033[32m✅ %s\033[0m\n' "$*"; } bad() { printf ' \033[31m❌ %s\033[0m\n' "$*"; } warn() { printf ' \033[33m⚠️ %s\033[0m\n' "$*"; } step() { printf '\n==> %s\n' "$*"; } OS=$(uname -s) step "环境" echo " 系统: $OS $(uname -r)" # ---------- 1. mDNS ---------- step "1/4 检查 mDNS(.local 域名解析)" case "$OS" in Darwin) ok "macOS 自带 Bonjour,$PI_NAME 可直接用" ;; Linux) if getent hosts "$PI_NAME" >/dev/null 2>&1; then ok "已能解析 $PI_NAME -> $(getent hosts "$PI_NAME" | awk '{print $1}' | tr '\n' ' ')" elif systemctl is-active --quiet avahi-daemon 2>/dev/null; then warn "avahi 在跑,但暂时解析不到 $PI_NAME(Pi 可能没插线/没开机)" else bad "本机缺少 mDNS 支持,$PI_NAME 解析不了" echo " 装一下(Debian/Ubuntu): sudo apt install libnss-mdns avahi-daemon" echo " 或者退回固定锚点: 见下面 pi-anchor 别名" fi ;; *) warn "未知系统,跳过 mDNS 检查" ;; esac # ---------- 2. 密钥 ---------- step "2/4 检查 SSH 密钥" mkdir -p "$HOME/.ssh"; chmod 700 "$HOME/.ssh" if [[ -f "$KEY" ]]; then ok "已有 $KEY" else if ssh-keygen -t ed25519 -N '' -f "$KEY" >/dev/null 2>&1; then ok "已生成新密钥 $KEY" warn "这是新电脑的新密钥:Pi 上默认没有它。两种办法登录——" echo " (a) 用密码登录一次,然后 ssh-copy-id $PI_USER@$PI_NAME" echo " (b) 直接把老电脑的 $KEY 拷过来" else bad "密钥生成失败,请手动 ssh-keygen -t ed25519" fi fi # ---------- 3. ssh config ---------- step "3/4 写入 ~/.ssh/config" if grep -qE "^Host\s+.*\bpi\b" "$CFG" 2>/dev/null; then ok "已存在 pi 相关配置,跳过(没动你的文件)" else { echo "" echo "# 连树莓派用的 SSH 别名 —— 由 setup-client.sh 添加" echo "# 用法: ssh pi (走 mDNS 名称,换网线/换地址都自动跟随)" echo "# ssh pi-anchor (走固定锚点,需要本机有 192.168.77.x 地址)" echo "Host pi $PI_NAME" echo " HostName $PI_NAME" echo " User $PI_USER" echo " IdentityFile ~/.ssh/id_ed25519" echo " IdentitiesOnly yes" echo " ServerAliveInterval 30" echo " StrictHostKeyChecking accept-new" echo "" echo "Host pi-anchor" echo " HostName $PI_ANCHOR" echo " User $PI_USER" echo " IdentityFile ~/.ssh/id_ed25519" echo " IdentitiesOnly yes" echo " StrictHostKeyChecking accept-new" } >> "$CFG" chmod 600 "$CFG" ok "已写入 $CFG(pi / pi-anchor)" fi # ---------- 4. 探测 ---------- step "4/4 探测树莓派" try_ssh() { ssh -o BatchMode=yes -o ConnectTimeout=6 -o StrictHostKeyChecking=accept-new \ "$PI_USER@$1" 'echo $(hostname)' 2>/dev/null } CONNECTED="" if H=$(try_ssh "$PI_NAME") && [[ -n "$H" ]]; then ok "mDNS 通路可用:ssh $PI_USER@$PI_NAME (主机名 $H)" CONNECTED=1 else bad "mDNS 通路不通:$PI_USER@$PI_NAME" fi if [[ -z "$CONNECTED" ]]; then if ip -4 -o addr 2>/dev/null | grep -q "$PI_ANCHOR_NET\." || \ ifconfig 2>/dev/null | grep -q "inet $PI_ANCHOR_NET\."; then if H=$(try_ssh "$PI_ANCHOR") && [[ -n "$H" ]]; then ok "锚点通路可用:ssh $PI_USER@$PI_ANCHOR (主机名 $H)" CONNECTED=1 else bad "锚点通路不通:$PI_USER@$PI_ANCHOR" fi else warn "本机没有 $PI_ANCHOR_NET.x 地址,锚点通路用不了" echo " 想用它就手工加一个地址(需要管理员权限):" case "$OS" in Darwin) echo " sudo ifconfig \$(route -n get default | awk '/interface/{print \$2}') alias $ANCHOR_CLIENT_IP netmask 255.255.255.0" ;; *) echo " sudo ip addr add $ANCHOR_CLIENT_IP/24 dev <你的网卡名>" ;; esac fi fi if [[ -z "$CONNECTED" ]]; then echo warn "两条路都不通。快速排查:" echo " · 网线插好了吗?Pi 的网口灯亮吗?(Pi 5 千兆口支持任意网线,不需要交叉线)" echo " · 看本机邻居表里有没有可疑的 Pi:" case "$OS" in Darwin) echo " arp -a | grep -E '169\\.254|$PI_ANCHOR_NET|10\\.42\\.'" ;; *) echo " ip neigh | grep -E '169\\.254|$PI_ANCHOR_NET|10\\.42\\.'" ;; esac echo " · 找不到就用 IPv6 广播 ping 整条链路:" echo " ping6 -c3 ff02::1%<你的网卡名> # macOS/Linux" echo " · 还是没有:Pi 没开机,或者它卡在启动 / 别的问题" exit 2 fi echo ok "搞定。以后一条命令: ssh pi"
```
五、重启验证
断电重启是这个方案里没法在运行中验证的一环,所以单独测了一次:
- 确实重启了:开机时间从 `
04:23:06` 变成 `05:21:51`。 - eth0 地址:重启前是静态锚点、DHCP 地址、`
fe80::…` 三个,重启后完全一致,全部自动恢复。 - 默认路由:`
via 10.42.0.1`(走电脑共享上网),一致。 - avahi 与 ssh:都是 active,且自启。
- `
ssh pi`:可用,Pi 出网也正常。 - 多余的 169.254:无。有 DHCP 时本来就不该有。
重启耗时:kernel `5.357s` + userspace `44.844s`,掉线到 SSH 恢复大约 30 秒。
结论:配置是持久的,拔电、换线、换电脑都不需要再动 Pi。
六、换一台新电脑怎么连
跑一遍 `setup-client.sh`(见上一节)。手动做的话是这三步:
```bash# 1) 先看通路在不在 ping -c1 pi.local arp -a | grep -E '169\.254|192\.168\.77|10\.42\.' # .local 不通时用这个找地址 # 2) 新电脑的公钥还没装到 Pi 上时,先用密码登录一次 ssh-copy-id pi@pi.local # 3) 写 ssh 别名,以后直接用 ssh pi cat >> ~/.ssh/config <<'EOF' Host pi HostName pi.local User pi EOF
```
第三步里 `HostName` 用 `pi.local`;如果这台电脑解析不了 `.local`,把它改成 `192.168.77.1`(网卡先设静态 `192.168.77.2/24`)即可。
`.local` 解析需要 mDNS:Debian / Ubuntu 上 `sudo apt install libnss-mdns avahi-daemon`。Ubuntu 22.04 之后的 systemd-resolved 通常也能直接解析。装不了就用锚点地址。
Windows 10 / 11 自带 mDNS 解析和 OpenSSH,基本开箱可用:
```powershell# 1) 探测 ping pi.local ssh pi@pi.local # 首次问指纹,输 yes;没有密钥就用密码 # 2) 万一 .local 不通,从邻居表里找地址 arp -a | findstr "169.254 192.168.77 10.42." # 3) 写别名:%USERPROFILE%\.ssh\config
```
实在解析不了 `pi.local` 时,给网卡设静态地址 `192.168.77.2 / 255.255.255.0`,然后 `ssh pi@192.168.77.1`。
七、连不上时的排查顺序
```bash# ① 名字能不能解析(能解析说明 avahi 在,只是路可能不通) avahi-resolve -n pi.local # ② 邻居表里找 Pi:三条通路各自的网段 ip neigh | grep -E '169\.254|192\.168\.77|10\.42\.' # Linux arp -a # macOS / Windows # ③ 整条链路 IPv6 广播 ping,最底层的"有人吗" ping6 -c3 ff02::1%<你的网卡名> # 会列出链路上所有 IPv6 主机 # ④ mDNS 彻底不通时,用锚点(客户端先配 192.168.77.2/24) ssh pi@192.168.77.1
```
网口灯不亮就是物理层问题,先换线换口。
实测遇到过一次 `
ssh: Could not resolve hostname pi.local: Temporary failure in name resolution`。 那是客户端 mDNS 解析的瞬时抖动,当时 Pi 完全正常(直连 IP 与 IPv6 广播 ping 都通),重试一次或者直接用 IP 连就好。 mDNS 只是方便,不是非它不可 —— 这也是锚点地址一直留着的理由。
八、几条运维备注
- 登录 shell 是 fish 的话:`
ssh pi@pi.local '带*通配符的命令'` 会被 fish 解析而报错,脚本化时写成 `ssh pi@pi.local 'bash -s' <<'EOF'` 这种显式 bash 的形式。 - 两条默认路由的 metric:同时接了有线与 WiFi 时,metric 小的优先(netplan 里可以给 WiFi 设 50、有线设 100),于是插着网线也可能走 WiFi。自己按需要调。
- 换硬件或重刷系统之后 SSH 指纹会变:客户端执行 `
ssh-keygen -R pi.local` 清掉旧记录。 - `
optional: true` 别省:没有它时,DHCP 等不到地址会拖慢开机(systemd-networkd 要等超时)。
九、怎么撤销
```bashsudo rm -f /etc/netplan/70-eth0-fallback.yaml && sudo netplan apply # 需要的话再还原云初始化的那份 sudo cp /root/50-cloud-init.yaml.bak /etc/netplan/50-cloud-init.yaml
```
参考
- `
systemd.network` — `LinkLocalAddressing=`:DHCP 失败才补 IPv4LL 那条语义的出处 - netplan 文档:`
link-local`、`optional` 等键的说明 - RFC 3927:IPv4 link-local 地址(`
169.254.0.0/16`)的规范