处于某些奇妙的原因,我需要使用一台 Linux 来共享网络给其他设备使用,但 NetworkManager 不会设置 ipv6 的 NAT 功能,这导致我没办法在该网络在使用 IPv6 访问家里的飞牛或者其他设备,于是就自己手搓了一份脚本来实现这个功能。

基础信息如下:

网卡 ip 作用
eno1 10.20.0.1 出口网络
enp2s0 192.168.123.1 共享网络

开启网络共享

NetworkManager 已经支持创建共享网络了,只用创建一个链接,将 IPv4 设置为共享即可。我使用的是 nmtui 命令编辑的链接,如果是使用 GNOME,可以直接在图形界面里将 IPv4 设置为共享,保存即可。

但是需要注意,这个功能依赖 dnsmasq 来提供 DHCP 和 DNS 服务,如果系统没有安装 dnsmasq,需要先安装它。

Radvd

/etc/radvd.conf 配置文件,为 enp2s0 接口广播 ULA 前缀 fd42:1234:5678::/64,并指定 RDNSS 服务器地址为 fd6d:d05e:e029::1。

interface enp2s0 {
AdvSendAdvert on;
MinRtrAdvInterval 30;
MaxRtrAdvInterval 100;
AdvDefaultPreference medium;
AdvHomeAgentFlag off;

prefix fd42:1234:5678::/64 {
AdvOnLink on;
AdvAutonomous on;
AdvRouterAddr on;
};

RDNSS fd6d:d05e:e029::1 {
AdvRDNSSLifetime 900;
};
};

NetworkManager

使用这个脚本可以在 NetworkManager 创建的共享网络接口上实现 IPv4 和 IPv6 的 NAT 功能,同时配合 radvd 广播 IPv6 前缀。

/etc/NetworkManager/dispatcher.d/99-nm-share-firewall.sh 脚本内容如下:

#!/bin/bash
# NetworkManager dispatcher script for shared network firewall + radvd + IPv6 NAT

IF="$1"
STATUS="$2"

# 物理出口网卡
PHYS_IF="eno1"
# 共享网络接口
SHARE_IF="enp2s0"

# IPv4 内网网段
SHARE_NET="192.168.123.0/24"
# IPv6 内网前缀(radvd 配置的 ULA)
SHARE_IPV6="fd42:1234:5678::/64"
# IPv6 网关地址
SHARE_IPV6_ADDR="fd42:1234:5678::1/64"

# 日志文件
LOG_FILE="/var/log/nm-share-firewall.log"

log() {
echo "[$(date)] $1" >> "$LOG_FILE"
}

add_firewall() {
log "Enabling IPv4/IPv6 forwarding and NAT for $SHARE_IF..."

# 开启 IPv4/IPv6 转发
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv6.conf.all.forwarding=1

# 给共享接口加上 IPv6 地址
if ! ip -6 addr show dev "$SHARE_IF" | grep -q "${SHARE_IPV6_ADDR%/*}"; then
ip -6 addr add "$SHARE_IPV6_ADDR" dev "$SHARE_IF"
fi

# 确保接口有IPv4地址(作为网关)
if ! ip addr show dev "$SHARE_IF" | grep -q "inet 192.168.123"; then
ip addr add 192.168.123.1/24 dev "$SHARE_IF"
fi

# ========== 核心:只做 NAT,不干扰路由 ==========

# 1. 允许所有转发(让路由表决定转发决策)
iptables -A FORWARD -i "$SHARE_IF" -j ACCEPT
iptables -A FORWARD -o "$SHARE_IF" -j ACCEPT

# 2. 仅对通过物理接口出去的流量做 MASQUERADE
iptables -t nat -A POSTROUTING -o "$PHYS_IF" -s "$SHARE_NET" -j MASQUERADE

# IPv6 同样处理
ip6tables -A FORWARD -i "$SHARE_IF" -j ACCEPT
ip6tables -A FORWARD -o "$SHARE_IF" -j ACCEPT
ip6tables -t nat -A POSTROUTING -o "$PHYS_IF" -s "$SHARE_IPV6" -j MASQUERADE

# 启动 radvd
if systemctl is-active --quiet radvd; then
systemctl restart radvd
else
systemctl start radvd
fi

log "Firewall rules added (simple NAT-only mode)"
}

remove_firewall() {
log "Removing IPv4/IPv6 firewall rules for $SHARE_IF..."

# 清理规则
iptables -D FORWARD -i "$SHARE_IF" -j ACCEPT 2>/dev/null
iptables -D FORWARD -o "$SHARE_IF" -j ACCEPT 2>/dev/null
iptables -t nat -D POSTROUTING -o "$PHYS_IF" -s "$SHARE_NET" -j MASQUERADE 2>/dev/null

ip6tables -D FORWARD -i "$SHARE_IF" -j ACCEPT 2>/dev/null
ip6tables -D FORWARD -o "$SHARE_IF" -j ACCEPT 2>/dev/null
ip6tables -t nat -D POSTROUTING -o "$PHYS_IF" -s "$SHARE_IPV6" -j MASQUERADE 2>/dev/null

# 停止 radvd
systemctl stop radvd

log "Firewall rules removed"
}

case "$STATUS" in
up)
if [ "$IF" == "$SHARE_IF" ]; then
add_firewall
fi
;;
down)
if [ "$IF" == "$SHARE_IF" ]; then
remove_firewall
fi
;;
esac

exit 0

成果展示

tailscale 打洞 飞牛直连