背景
在特定网络环境中,可能存在如下场景:电脑A能够直接访问服务器A,但无法直接访问服务器B,而服务器A与服务器B之间通信无障碍。为了使电脑A能够访问服务器B,通常有两种方法:
- 手动中转:用户首先从电脑A登录到服务器A,然后从服务器A再次登录到服务器B。这种方法虽然可行,但操作繁琐,效率低下。
- 端口转发:利用服务器A作为中介,设置端口转发规则,将来自电脑A的请求通过服务器A转发至服务器B。完成配置后,电脑A可以直接向指定端口发送请求,实现对服务器B的间接访问。这种方式不仅简化了访问流程,还提高了工作效率。
graph TB
subgraph "网络环境"
direction LR
A[电脑A]:::client
B[服务器A]:::server
C[服务器B]:::server
end
A -->|直接访问| B
A --x|禁止访问| C
B -->|可访问| C
classDef client fill:#87CEFA,stroke:#1E90FF,stroke-width:3px,rounded-corners;
classDef server fill:#FFDAB9,stroke:#FFA07A,stroke-width:3px,rounded-corners;
class A client;
class B,C server;
%% 增加箭头的样式
linkStyle 0 stroke:#228B22,stroke-width:3px;
linkStyle 1 stroke:#FF6347,stroke-width:3px,stroke-dasharray: 5,5;
linkStyle 2 stroke:#228B22,stroke-width:3px;
端口转发
在 Linux 下,可以通过多种方式实现端口转发,常见的方法包括使用 iptables 和 sshd 配置。这里我们使用和讨论的是iptables。
iptables流程
graph LR
A[进入数据包] --> B[PREROUTING]
B --> C[路由决策]
C -->|本地目标| D[INPUT]
C -->|非本地目标| E[FORWARD]
D --> F[本地进程处理]
F --> G[路由决策]
G --> H[OUTPUT]
H --> I[POSTROUTING]
E --> I[POSTROUTING]
classDef inputOutput fill:#f9f,stroke:#333,stroke-width:2px;
class D,F,G,H,I inputOutput;
classDef forward fill:#ccf,stroke:#333,stroke-width:2px;
class E forward;
| 链 | 作用 | 流量类型 | 使用场景 |
|---|---|---|---|
INPUT | 处理目标是本机的数据包 | 进入本机的数据包 | 控制哪些流量可以访问本机的服务 |
FORWARD | 处理转发的数据包 | 目标是其他主机的流量 | 控制路由器或网关转发的流量 |
OUTPUT | 处理本机发出的数据包 | 本机生成并发出的流量 | 控制本机发出的流量 |
PREROUTING | 处理进入系统的数据包,在路由前进行处理 | 进入本机的数据包,路由之前 | 用于端口转发、目标地址转换(DNAT)等 |
POSTROUTING | 处理离开系统的数据包,在路由后进行处理 | 离开本机的数据包,路由之后 | 用于源地址转换(SNAT)、出口流量修改等 |
分析
| 端 | IP |
|---|---|
| 电脑 A | 192.168.31.149 |
| 服务器 A | 172.30.243.12 |
| 服务器 B | 192.168.31.132 |
要实现的是:电脑A访问的是服务器A的10022端口,等于访问服务器B的22端口,就是将服务器A端口10022转发到服务器22端口上。
先尝试用电脑A去telnet服务器A的10022看看
PS C:\Users\22787> telnet 172.31.88.37 10022
正在连接172.31.88.37...无法打开到主机的连接。 在端口 10022: 连接失败
PS C:\Users\22787>
在服务器A通过 tcpdump 抓包看看,可以发现发送了5次都被rst。
root@root:~# tcpdump -i any port 10022 -n
tcpdump: WARNING: any: That device doesn't support promiscuous mode
(Promiscuous mode not supported on the "any" device)
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
00:34:49.114827 eth0 In IP 172.30.240.1.63532 > 172.30.243.12.10022: Flags [S], seq 4070433852, win 65535, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
00:34:49.114867 eth0 Out IP 172.30.243.12.10022 > 172.30.240.1.63532: Flags [R.], seq 0, ack 4070433853, win 0, length 0
00:34:49.619129 eth0 In IP 172.30.240.1.63532 > 172.30.243.12.10022: Flags [S], seq 4070433852, win 65535, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
00:34:49.619174 eth0 Out IP 172.30.243.12.10022 > 172.30.240.1.63532: Flags [R.], seq 0, ack 1, win 0, length 0
00:34:50.131152 eth0 In IP 172.30.240.1.63532 > 172.30.243.12.10022: Flags [S], seq 4070433852, win 65535, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
00:34:50.131192 eth0 Out IP 172.30.243.12.10022 > 172.30.240.1.63532: Flags [R.], seq 0, ack 1, win 0, length 0
00:34:50.631726 eth0 In IP 172.30.240.1.63532 > 172.30.243.12.10022: Flags [S], seq 4070433852, win 65535, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
00:34:50.631766 eth0 Out IP 172.30.243.12.10022 > 172.30.240.1.63532: Flags [R.], seq 0, ack 1, win 0, length 0
00:34:51.144974 eth0 In IP 172.30.240.1.63532 > 172.30.243.12.10022: Flags [S], seq 4070433852, win 65535, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
00:34:51.145011 eth0 Out IP 172.30.243.12.10022 > 172.30.240.1.63532: Flags [R.], seq 0, ack 1, win 0, length 0
通过在iptablse添加规则,查看上面telnet走了那个链路。
## 添加规则到 `INPUT` 链
sudo iptables -A INPUT -p tcp --sport 10022 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 10022 -j ACCEPT
## 添加规则到 `OUTPUT` 链
sudo iptables -A OUTPUT -p tcp --sport 10022 -j ACCEPT
sudo iptables -A OUTPUT -p tcp --dport 10022 -j ACCEPT
## 添加规则到 `FORWARD` 链
sudo iptables -A FORWARD -p tcp --sport 10022 -j ACCEPT
sudo iptables -A FORWARD -p tcp --dport 10022 -j ACCEPT
## 添加规则到 `PREROUTING` 链
sudo iptables -t nat -A PREROUTING -p tcp --sport 10022 -j REDIRECT --to-ports 10022
sudo iptables -t nat -A PREROUTING -p tcp --dport 10022 -j REDIRECT --to-ports 10022
## 添加规则到 `POSTROUTING` 链
sudo iptables -t nat -A POSTROUTING -p tcp --sport 10022 -j SNAT --to-source :10022
sudo iptables -t nat -A POSTROUTING -p tcp --dport 10022 -j SNAT --to-source :10022
查看链路信息 iptables -L -v -n:
root@root:~# sudo iptables -L -v -n
Chain INPUT (policy ACCEPT 28 packets, 2492 bytes)
pkts bytes target prot opt in out source destination
0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp spt:10022
0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:10022
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp spt:10022
0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:10022
Chain OUTPUT (policy ACCEPT 21 packets, 3508 bytes)
pkts bytes target prot opt in out source destination
0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp spt:10022
0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:10022
查看PREROUTING链路信息 iptables -t nat -L PREROUTING -v -n:
root@root:~# sudo iptables -t nat -L PREROUTING -v -n
Chain PREROUTING (policy ACCEPT 4 packets, 813 bytes)
pkts bytes target prot opt in out source destination
0 0 REDIRECT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp spt:10022 redir ports 10022
0 0 REDIRECT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:10022 redir ports 10022
查看POSTROUTING链路信息 iptables -t nat -L POSTROUTING -v -n:
root@root:~# sudo iptables -t nat -L POSTROUTING -v -n
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 SNAT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp spt:10022 to::10022
0 0 SNAT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:10022 to::10022
再次telnet看看数据包走了哪些链路:
root@root:~# sudo iptables -L -v -n
Chain INPUT (policy ACCEPT 359 packets, 24340 bytes)
pkts bytes target prot opt in out source destination
0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp spt:10022
5 260 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:10022
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp spt:10022
0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:10022
Chain OUTPUT (policy ACCEPT 263 packets, 29108 bytes)
pkts bytes target prot opt in out source destination
5 200 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp spt:10022
0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:10022
root@root:~# sudo iptables -t nat -L PREROUTING -v -n
Chain PREROUTING (policy ACCEPT 4 packets, 813 bytes)
pkts bytes target prot opt in out source destination
0 0 REDIRECT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp spt:10022 redir ports 10022
5 260 REDIRECT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:10022 redir ports 10022
root@root:~# sudo iptables -t nat -L POSTROUTING -v -n
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 SNAT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp spt:10022 to::10022
0 0 SNAT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:10022 to::10022
通过上面可以看到 5次包走 PREROUTING-> INPUT-> OUTPUT, FORWARD没走因为目标地址172.30.243.12是本机,POSTROUTING没走因为目标地址172.30.243.12是本机,数据包没走出去。
我们要实现端口转发的话,首先将目标端10022和目标地址172.30.243.12(服务器A),改成目标端22和目标地址192.168.31.132(服务器B),通过PREROUTING链路修改
sudo iptables -t nat -I PREROUTING -p tcp --dport 10022 -j DNAT --to-destination 192.168.31.132:22
因为我们修改了目标地址,目标地址非本机地址导致链路变了:PREROUTING-> FORWARD-> PREROUTING
我们允许数据包通过FORWARD链
sudo iptables -A FORWARD -p tcp --dport 22 -j ACCEPT
为了让数据包能找到回来的路,需要SNAT就是源地址转换(如果两个设备都在同一个私有网络或子网内,它们之间的流量可以直接路由,不需要进行 SNAT),172.30.243.12和192.168.31.132不是同一个网络。
sudo iptables -t nat -A POSTROUTING -d 192.168.31.132 -j SNAT --to-source 172.30.243.12
还可以这样写,从eth0出去的数据包,都会被SNAT,转换的源地址是eth0的ip
sudo iptables -t nat -I POSTROUTING -o eth0 -j MASQUERADE
如果默认FORWARD是允许所有数据包通过的话,上面就完成端口转发了,可以telnet了。
如果我们给FORWARD添加一个拒绝所有流量规则:
sudo iptables -A FORWARD -j DROP
你会发现telnet就不行,那是你没有允许回来的数据包通过,这个是往往容易忽略的,添加一个允许通过:
sudo iptables -I FORWARD -p tcp --sport 22 -j ACCEPT
这样就可以了。
总结
- 目标地址是本机的链路:
PREROUTING->INPUT->OUTPUT。 - 目标地址不是本机的链路:
PREROUTING->FORWARD->POSTROUTING。 - 在
PREROUTING中nat转换目标不是本机地址,转换的地址和本机地址不是通过网络,则需要在POSTROUTING中SNAT转换。 - 注意回来数据包会走
FORWARD。