提示

iptables 双网口转发实战:我把一台虚拟机变成了路由器

一台只有内网 IP 的服务器想访问外网,又不能直接配网关。我手头正好有一台双网口的虚拟机,决定拿它当“路由器”做 NAT 转发。本以为 iptables 配两条规则就完事,结果折腾了一下午——开了转发没开 NAT,配了 SNAT 忘了配路由……这篇文章把整个过程和踩的坑全记下来,方便以后自己查。

一、场景描述

  • 一台 server 虚拟机:双网卡
  • eth0: 172.25.254.224(可以访问外网/同网段其他机器)
  • eth1: 1.1.1.224(连接 client 的“内网侧”)
  • 一台 client 虚拟机:单网卡
  • eth0: 172.25.254.124(原本只能和同网段通信,无法访问其他网段)

目标:让 client(172.25.254.124)能通过 server 访问外部网络,并且外部也能通过 server 访问 client 的特定端口(比如 SSH)。

说白了就是:server 做网关 + NAT + 端口转发

二、准备工作:关闭 firewalld,启用 iptables

CentOS 7 以后默认用 firewalld,但用 iptables 更直观(至少我觉得)。

server 上执行:

systemctl stop firewalld
systemctl mask firewalld
systemctl start iptables.service
systemctl enable iptables.service

踩坑:如果不执行 mask,firewalld 可能被其他服务拉起来,和 iptables 冲突。我遇到过明明 iptables 加了规则,但就是不生效,最后发现 firewalld 又活了。

三、iptables 常用参数速查(我记不全,列个表)

参数           含义                     小抄
-t       指定表(默认 filter)         -t nat 常用
-nL      列出规则,不解析 IP            iptables -nL
-A       追加规则(放最后)              -A INPUT
-I       插入规则(可指定位置)            -I INPUT 2
-D       删除规则                       -D INPUT 3
-R       替换规则                       -R INPUT 2 -s x.x -j ACCEPT
-F       清空规则                       iptables -F
-P       修改默认策略                 -P INPUT DROP
-s       源地址                            -s 172.25.254.10
-p       协议                         -p tcp
--dport  目的端口                       --dport 22
-j       动作                         ACCEPT / REJECT / DROP / SNAT / DNAT
-o       出口网卡                       -o eth0
-i       入口网卡                       -i eth0

四、基础操作:filter 表练手(先热热身)

在 server 上先玩玩 filter 表,熟悉增删改查:

4.1 允许本地回环

iptables -t filter -A INPUT -i lo -j ACCEPT
iptables -nL   # 查看

4.2 允许特定 IP 访问所有端口

iptables -A INPUT -s 172.25.254.10 -j ACCEPT

4.3 拒绝特定 IP

iptables -A INPUT -s 172.25.254.10 -j REJECT

踩坑:如果上面那条 ACCEPT 在前面,REJECT 永远不会执行。iptables 是按顺序匹配的,匹配到就停止。所以要把精细规则放前面,默认策略放最后。
4.4 删除、替换、插入规则

# 删除第 3 条规则
iptables -D INPUT 3

# 修改第 2 条规则:把 ACCEPT 改成 REJECT
iptables -R INPUT 2 -s 172.25.254.10 -j REJECT

# 在第 2 条位置插入一个规则(允许特定 IP 的 SSH)
iptables -I INPUT 2 -s 172.25.254.10 -p tcp --dport 22 -j ACCEPT

4.5 修改默认策略

iptables -P INPUT DROP   # 丢掉所有没匹配到的包
# 然后只有上面明确 ACCEPT 的 IP 能连
iptables -P INPUT ACCEPT # 改回来

踩坑:千万别在远程连接时直接 -P INPUT DROP 然后忘记加允许 SSH 的规则。我这么干过,只能让同事去机房重启。

五、核心:地址伪装(SNAT)——让 client 能上网

现在开始正经配置。

第一步:开启路由转发

echo 1 > /proc/sys/net/ipv4/ip_forward
# 永久生效的话改 /etc/sysctl.conf: net.ipv4.ip_forward=1

第二步:在 server 上添加 SNAT 规则

把 client 的流量“伪装”成 server 的 eth0 地址出去:

iptables -t nat -A POSTROUTING -o eth0 -j SNAT --to-source 172.25.254.224

解释:

1、POSTROUTING:路由之后,离开网卡之前

2、-o eth0:从 eth0 出去的包

3、SNAT --to-source:把源地址改成 172.25.254.224

这样,client(172.25.254.124)往外发包时,源地址会被替换成 server 的 eth0 IP。外部服务器回包会回到 server,server 再转发给 client。

测试:在 client 上 ping 8.8.8.8。如果通了,并且在 server 上用 w -i 看到 client 的访问源 IP 显示为 172.25.254.224,说明伪装成功。

踩坑:如果只开了 ip_forward 而忘了配 SNAT,client 能发出包,但回包到 server 后,server 不知道发给谁(因为源地址是 client 的真实 IP,不在同一子网),直接丢包。

六、端口转发(DNAT)——让外部能访问 client 的 SSH

需求:外网用户 SSH 到 server 的 172.25.254.224:22,实际被转发到 client 的 1.1.1.124:22。

注意:这里 client 的 IP 是 1.1.1.124(server 的 eth1 同网段),外网不能直接访问它。

在 server 上添加 DNAT 规则:

iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 22 -j DNAT --to-dest 1.1.1.124:22

解释:

1、PREROUTING:路由之前,刚进网卡

2、-i eth0:从 eth0 进来的包

3、DNAT --to-dest:把目的地址改成 1.1.1.124:22

测试:从外部机器(比如你的笔记本)执行 ssh 172.25.254.224,应该会登录到 client 而不是 server。

踩坑:如果 client 的默认网关不是 server 的 eth1(1.1.1.224),那么 client 的回包会走自己的默认网关,导致连接失败。解决方法:在 client 上添加一条路由,或者把 client 的默认网关设为 server 的 eth1 IP。

七、管理 NAT 规则

查看 NAT 表:

iptables -t nat -nL
删除第一条 PREROUTING 规则:
iptables -t nat -D PREROUTING 1
删除第一条 POSTROUTING 规则:
iptables -t nat -D POSTROUTING 1

问题汇总(全都是真实踩过的)

  1. 开启 ip_forward 后 client 依然不能上网
    现象:client 能 ping 通 server 的 eth1(1.1.1.224),但 ping 不通 8.8.8.8。
    原因:很可能忘了配 SNAT,或者 SNAT 规则配错了网卡名/源地址。
    检查:在 server 上 tcpdump -i eth0 icmp,看有没有发出 client 的 ping 包。如果发出去了但源地址是 client 原始 IP(172.25.254.124),说明 SNAT 没生效。

  2. 端口转发后 SSH 卡在 “connection refused”
    现象:ssh 172.25.254.224 能连,但输入密码后卡住或直接拒绝。
    原因:client 的回包路由不对。client 收到请求后,回复的源 IP 是它的 eth0(1.1.1.124),但目标 IP 是外部机器的 IP,如果没有路由,回包会走默认网关(可能不是 server)。
    解决:在 client 上把默认网关设为 server 的 eth1:

ip route add default via 1.1.1.224

或者更精细地:只给外部 IP 段加路由。

  1. iptables -P INPUT DROP 后把自己踢出去了
    现象:远程 SSH 断开,再也连不上。
    原因:没有提前加 -A INPUT -p tcp --dport 22 -j ACCEPT。
    修复:只能去物理机/控制台执行 iptables -P INPUT ACCEPT。
    教训:修改默认策略前,先确认有一条允许当前连接的规则。

  2. 使用 --dport 时没有指定协议 -p
    现象:规则加进去了,但就是不匹配。
    原因:--dport 必须配合 -p tcp 或 -p udp 使用,不然 iptables 不知道是哪个协议的端口。
    正确写法:

iptables -A INPUT -p tcp --dport 22 -j ACCEPT
  1. 清空 NAT 表后用 service iptables save 保存不上
    现象:iptables -F 清空后,重启 iptables 服务规则又回来了。
    原因:iptables 规则保存在 /etc/sysconfig/iptables,-F 只清内存,不写文件。
    解决:
service iptables save
或者手动 iptables-save > /etc/sysconfig/iptables。

九、一点小建议

做任何路由转发前,先用 tcpdump -i eth0 icmp 抓包看流向。比瞎猜快多了。

SNAT 和 DNAT 一定要搞清楚方向:

SNAT:改源地址(让出去的数据包像是我发的)

DNAT:改目的地址(让进来的数据包被转给别人)

如果只是临时测试,可以在 iptables 规则前加 -t 30 定时自动清空,防止把自己锁外面。

保存好你的 iptables 脚本,我习惯写成 iptables-restore < rules.txt 的形式,比一条条敲省事。

写完这篇博客,我对 iptables 的恐惧少了一分。其实双网口转发就那么几个关键点:ip_forward + SNAT + DNAT。剩下的都是细节和坑。

顺便说一句,别在生产环境直接用 -P INPUT DROP。

顶部
×
🔖
收藏本站
将本站添加到浏览器书签,方便下次访问
Ctrl + D (Windows/Linux)
+ D (Mac)