浅谈代理:程序中转、NAT和封装

Posted by SingChia Blog on March 23, 2018

1. 背景

本文所讨论的代理是计算机网络范畴的概念,在维基百科代理服务的名词定义:

Proxy server, a computer network service that allows clients to make indirect network connections to other network services.
译:代理服务器,一种允许客户端间接地连接到其他网络服务的服务。

代理所做的正是将A不能或者不好到达C的流量:

A !=> C

通过代理B来到达:

A => B => C 

代理之所以重要是因为从普通用户到专业运维都会不可避免地跟它打交道。 普通用户可能需要访问正常不能访问的网站,或者希望自己玩游戏不会卡顿。专业运维可能需要用更方便地方式访问部署服务的外网服务器,或者希望外网服务器进入的流量能够更均衡或者更高可用地落到真正提供服务的内网服务器上。

本文主要从工具和原理等方面来谈一谈代理

2. ssh端口转发

本节所讨论的sshOpenSSH SSH client,也是常用的远程登录程序。其客户端基本用法如下:

ssh [-1246AaCfGgKkMNnqsTtVvXxYy] \
[-b bind_address] [-c cipher_spec] [-D [bind_address:]port] \
[-E log_file] [-e escape_char] [-F configfile] [-I pkcs11] \
[-i identity_file] [-J [user@]host[:port]] [-L address] \
[-l login_name] [-m mac_spec] [-O ctl_cmd] [-o option] \
[-p port] [-Q query_option] [-R address] [-S ctl_path] \
[-W host:port] [-w local_tun[:remote_tun]] \
[user@]hostname [command]

2.1. ssh -L本地端口转发

场景描述:
A机器内网地址为:192.168.0.10
B机器公网地址为:220.220.0.10 内网地址为:172.16.0.11
C机器内网地址为:172.16.0.10
A能够访问B
B能够访问C
A不能访问C
B、C机器上都运行了sshd;A机器上有ssh客户端
现在需要A能够访问C机器上的1202上端口

本地转发需要使用-L选项。在sshman中有这样一段:

Specifies that connections to the given TCP port or Unix socket on the local (client) host are to be forwarded to the given host and port, or Unix socket, on the remote side. This works by allocating a socket to listen to either a TCP port on the local side, optionally bound to the specified bind_address, or to a Unix socket. Whenever a connection is made to the local port or socket, the connection is forwarded over the secure channel, and a connection is made to either host port hostport, or the Unix socket remote_socket, from the remote machine.

在主机C上使用nc来监听1202端口

> nc -l 172.16.0.10 1202

我们在主机A上:

> ssh -L 7777:172.16.0.10:1202 220.220.0.10

紧接着在主机A上连接本地7777端口并发送hello

> nc 127.0.0.1 7777
hello

在主机C上将会看到消息正常到达,即本机777端口的流量经过主机220.220.0.10转发给172.16.0.10的端口1202。 以下为各个主机中相关socket情况:

主机A

Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         	State       PID/Program name
tcp        0      0 127.0.0.1:7777          0.0.0.0:*               	LISTEN      26071/ssh
tcp        0      0 127.0.0.1:40900         127.0.0.1:7777          	ESTABLISHED 26677/nc
tcp        0      0 127.0.0.1:7777          127.0.0.1:40900         	ESTABLISHED 26071/ssh
tcp        0      0 192.168.0.10:48674      220.220.0.10:22         	ESTABLISHED 26071/ssh
tcp6       0      0 ::1:7777                :::*                    	LISTEN      26071/ssh

可以看出本机上ssh监听了7777端口并使用端口48674220.220.0.10:22建立了连接;nc使用端口40900777建立了连接。

主机B

Proto Recv-Q Send-Q Local Address           Foreign Address         	State       PID/Program name
tcp        0      0 0.0.0.0:22              0.0.0.0:*               	LISTEN      28336/sshd
tcp        0      0 220.220.0.10:22         192.168.0.10:48674      	ESTABLISHED 26072/sshd: root@pt
tcp        0      0 172.16.0.11:57922       172.16.0.10:1202        	ESTABLISHED 26072/sshd: root@pt
tcp6       0      0 :::22                   :::*                    	LISTEN      28336/sshd

可以看到一个来自主机A的连接以及一个内网连接172.16.0.11:57922172.16.0.10:1202

说明:有公网设备相关工作经验的读者肯定会疑惑为什么B公网地址A内网地址直接相通,请忽略这点,因为这个场景环境是使用netns(linux network namespace)搭建,实际上所有的地址都是内网地址。

主机C

Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         	State       PID/Program name
tcp        0      0 0.0.0.0:22              0.0.0.0:*               	LISTEN      28342/sshd
tcp        0      0 172.16.0.10:1202        172.16.0.11:57922       	ESTABLISHED 26571/nc
tcp6       0      0 :::22                   :::*                    	LISTEN      28342/sshd

抓包可以看出流量过程:A:40900 -> A:7777 => A:48674 -> B:22 => B:57922 -> C:1202

2.2. ssh -R远程端口转发

场景描述:
A机器公网地址为:220.220.0.10
B机器内网地址为:192.168.0.10
C机器内网地址为:172.16.0.10
B能够访问A
B能够访问C
A不能访问C
A、C机器上都运行了sshd;B机器上有ssh客户端
现在需要A能够访问C机器上的1202上端口

远程转发需要使用-R选项。在sshman中有这样一段:

Specifies that connections to the given TCP port or Unix socket on the remote (server) host are to be forwarded to the given host and port, or Unix socket, on the local side. This works by allocating a socket to listen to either a TCP port or to a Unix socket on the remote side. Whenever a connection is made to this port or Unix socket, the connection is forwarded over the secure channel, and a connection is made to either host port hostport, or local_socket, from the local machine.

在主机C上使用nc来监听1202端口

> nc -l 172.16.0.10 1202

我们在主机B上:

> ssh -R 7777:172.16.0.10:1202 220.220.0.10

紧接着在主机A上连接本地7777端口并发送hello

> nc 127.0.0.1 7777
hello

在主机C上将会看到消息正常到达,即本机777端口的流量经过主机192.168.0.10转发给172.16.0.10的端口1202。 以下为各个主机中相关socket情况:

主机A

Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         	State       PID/Program name
tcp        0      0 127.0.0.1:7777          0.0.0.0:*               	LISTEN      18636/sshd: root@pt
tcp        0      0 0.0.0.0:22              0.0.0.0:*               	LISTEN      28870/sshd
tcp        0      0 127.0.0.1:7777          127.0.0.1:40918         	ESTABLISHED 18636/sshd: root@pt
tcp        0      0 220.220.0.10:22         192.168.0.10:50718      	ESTABLISHED 18636/sshd: root@pt
tcp        0      0 127.0.0.1:40918         127.0.0.1:7777          	ESTABLISHED 18731/nc
tcp6       0      0 ::1:7777                :::*                    	LISTEN      18636/sshd: root@pt
tcp6       0      0 :::22                   :::*                    	LISTEN      28870/sshd

可以看出sshd监听了7777端口并接受来自主机B的连接220.220.0.10:22192.168.0.10:50718nc使用40918sshd监听的7777建立了连接。

主机B

Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         	State       PID/Program name
tcp        0      0 0.0.0.0:22              0.0.0.0:*               	LISTEN      28336/sshd
tcp        0      0 192.168.0.10:50718      220.220.0.10:22         	ESTABLISHED 18635/ssh
tcp        0      0 192.168.0.10:57940      172.16.0.10:1202        	ESTABLISHED 18635/ssh
tcp6       0      0 :::22                   :::*                    	LISTEN      28336/sshd

一个是上述主机B主机A的连接,一个是到主机C的连接。

主机C

Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         	State       PID/Program name
tcp        0      0 0.0.0.0:22              0.0.0.0:*               	LISTEN      28342/sshd
tcp        0      0 172.16.0.10:1202        192.168.0.10:57940       	ESTABLISHED 18605/nc
tcp6       0      0 :::22                   :::*                    	LISTEN      28342/sshd

抓包可以看出流量过程:A:40918 -> A:7777 => A:22 -> B:50718 => B:57940 -> C:1202

tcp是通的,那么自然http也通了,最常使用这种方法场景的是从公网访问内网的web服务器

3. SOCKS5协议

虽然端口转发可以解决某一个端口的代理问题,但我们还有很多动态端口转发的需求,即我们希望将目的包封装起来,发给代理服务代理服务收到后解包再按照该目的地址转发,只所以是动态,是因为代理服务会根据解包后目的地址不同建立不同的长连接。

有一个名为SOCKS代理协议比较流行,它定义了一种动态端口转发的协议。在我SOCKS5协议「RFC1928翻译」中可以了解细节。RFC1928描述的SOCKS5协议发布于1996,甚至早于1999HTTP/1.1(RFC2616)协议。

3.1. ShadowSocks

SOCKS5协议的实现很多,其中ShadowSocks因为The Great Firewall的原因被很多同学所使用。由于SOCKS5协议只描述了客户端到服务端之间的协议,因此在服务端如何把包发往目标机器是可以自己决定的。

ShadowSocks的做法是分为ss客户端ss服务端注意ss客户端ss服务端之间的通信并非SOCKS5协议实现,而客户端ss客户端的过程是符合SOCKS5协议的。即ss客户端SOCKS5服务端,客户端封包是由操作系统支持的,这里的客户端可以是浏览器。

如果读者本地也安装了ShadowSocksss客户端,会发现通常该ss客户端默认是监听本地127.0.0.1:1086端口的(虽然RFC提到通常使用1080端口)。如果你在lo网卡上抓包,访问google.com就可以抓到SOCKS5的包:

中间列为负载的16进制显示,右列为acsii显示,对比SOCKS5协议,可以发现:

>> 05 01 00                                           ...
#客户端发起SOCKS5(0x05)流程,认证方法占一个字节(0x01),无需认证(0x00)

<< 05 00                                              ..
#SOCKS5(0x05)服务端表示可以不认证(0x00)

>> 05 01 00 03 0e 77 77 77  2e 67 6f 6f 67 6c 65 2e   .....www .google.
#客户端发起SOCKS5(ox05)连接(0x01),保留一字节(0x00),地址类型为域名(0x03),域名13字节(0x0e)
>> 63 6f 6d 01 bb                                     com..
#域名和端口为www.google.com:443

<< 05 00 00 01 00 00 00 00  00 00                     ........ ..
#返回成功,未返回绑定的地址和端口,这在协议中本来就是可选的

而协商成功后包已经全都属于数据面了,即业务层的内容,给SOCKS5服务端会直接转发。

3.2. ssh -D动态端口转发

动态端口转发需要-D选项,在sshman中有这样一段:

Specifies a local “dynamic” application-level port forwarding. This works by allocating a socket to listen to port on the local side, optionally bound to the specified bind_address. Whenever a connection is made to this port, the connection is forwarded over the secure channel, and the application protocol is then used to determine where to connect to from the remote machine. Currently the SOCKS4 and SOCKS5 protocols are supported, and ssh will act as a SOCKS server. Only root can forward privileged ports. Dynamic port forwardings can also be specified in the configuration file.

可以看出ssh也是同样作为SOCKS5服务端工作的,读者也可以使用这种方式来实现动态端口转发。

4. NAT

上述的几种方法里都是从工具的角度来达到日常代理需求,原理来上说是利用程序中转,不够透明。再看代理的行为,A不能或者不好到达C的流量:

A !=> C

通过代理B来到达:

A => B => C 

说明这里B的网络与C是通的,我们还是用2.1节的场景来说明:

场景描述:
A机器内网地址为:192.168.0.10
B机器公网地址为:220.220.0.10 内网地址为:172.16.0.11
C机器内网地址为:172.16.0.10
A能够访问B
B能够访问C
A不能访问C

这次我们附上场景图:

此时再考虑前面章节所描述的工具,所有让AB的流量为了能够到C,在B上都会改变流量的目的ip地址Cip地址。

那么自然能想到最简单的就是NAT了,所谓NAT(RFC2663)地址转换协议,属于网络层,在RFC2663文档中描述了典型应用场景,局域网中的主机需要连通互联网则需要做地址转换,把私有ip转换成公网ip,反之依然。

RFC2663 4.1.1节中描述了基本NAT

With Basic NAT, a block of external addresses are set aside for translating addresses of hosts in a private domain as they originate sessions to the external domain. For packets outbound from the private network, the source IP address and related fields such as IP, TCP, UDP and ICMP header checksums are translated. For inbound packets, the destination IP address and the checksums as listed above are translated.

所以要实现一个基本NAT,除了最基本的地址转换外,还需要重新计算包的校验和

4.1. iptables

虽然NAT(RFC2663)的描述是在末端路由器上,但我们更关注服务器,netfilterlinux2.4.x+内核的包过滤框架,在这个框架中开发的软件允许包过滤、地址转换以及包修改。而iptables是其用户空间的命令行工具,可以用来配置和管理上述功能。

netfilter-hacking-HOWTO文档中描述了netfilter的架构,我把其中一张图拿出来如下:

   --->PRE------>[ROUTE]--->FWD---------->POST------>
   Conntrack    |       Mangle   ^    Mangle
   Mangle       |       Filter   |    NAT (Src)
   NAT (Dst)    |                |    Conntrack
   (QDisc)      |             [ROUTE]
                v                |
                IN Filter       OUT Conntrack
                |  Conntrack     ^  Mangle
                |  Mangle        |  NAT (Dst)
                v                |  Filter

由于我们把B充当代理服务器,所以当包从AB,在图中PRE ROUTE即服务器路由数据包之前把包的目的地址修改成C的,这个过程称为DNAT。而在C回包到B时,再把源地址修改成B的,这个过程称为SNAT,通常在POST ROUTE实现。

注意:对网络不熟悉的读者可能有点不明白其中细节,在我后面的博文中会有关于网络的介绍,读者可以更关注在实现上。

在主机C上使用nc来监听1202端口:

> nc -l 172.16.0.10 1202

在主机B上使用iptables来实现NAT220.220.0.10所在网卡为eth0172.16.0.11所在网卡为eth1

> sysctl -w net.ipv4.ip_forward=1 #允许内核站转发数据包,临时生效
> sysctl -w net.ipv4.conf.eth1.proxy_arp=1 #允许内网网卡转发arp
> iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 1202 -j DNAT --to-destination 172.16.0.10:1202
> iptables -t nat -A POSTROUTING -o eth0 -j SNAT -s 172.16.0.10 --to-source 220.220.0.10

在主机A上连接B:1202端口并发送hello

> nc 220.220.0.10 1202
hello

就可以在主机C收到该信息了。

以下为各个主机中相关socket情况:

主机A

Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      0 192.168.0.10:60114      220.220.0.10:1202       ESTABLISHED 6945/nc              off (0.00/0/0)

在主机A看来就是与B建立的长连接。

主机B

上没有这条tcp长连接,因为是NAT

主机C

Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      0 172.16.0.10:1202        192.168.0.10:60114      ESTABLISHED 6943/nc              off (0.00/0/0)

在主机C上能才能看到真实情况,因为我们没有对从B发过来的包做SNAT,所以在C看来所有的包都是与A直接相通的。

4.2. LVS

LVS(linux virtual server)章文嵩早年开发的负载均衡器,依赖于netfilter,所以其在内核态也需要跑一个模块,叫ip_vs,同样的,如果你的linux内核版本高于2.4.x,那么:

> modinfo ip_vs

是能够找到这个内核模块的,如果没有安装,请使用insmod来安装刚刚找到的模块。

netfilter类似,LVS提供了用户态工具来ipvsadm来控制LVS的行为,从负载均衡角度来说,ipvsadm还是比较容易使用的,我大概调研了下也是使用netlink来实现,但官网没有开放具体协议,所以目前使用工具。

把刚刚场景创建的iptables规则全部删除,此外在上述场景再添加一台目标机器D

D机器内网地址为:172.16.1.10

我们希望A发给B的流量能够无感均衡地发送到CD。需要注意的是由于后端的服务器已经超过1台,很多L4(传输层)和L7(应用层)的协议是有状态的,这就需要共享相同状态的流量需要发送到同一台后端服务器上,不过不用担心,LVS的内核模块都已经做好了。

B机器上:

> ipvsadm -A -t 220.220.0.10:80 #添加虚拟服务器
> ipvsadm -a -t 220.220.0.10:80 -r 172.16.0.10:80 -m #给虚拟服务器添加一台nat真实服务器
> ipvsadm -a -t 220.220.0.10:80 -r 172.16.1.10:80 -m #给虚拟服务器添加一台nat真实服务器

这样从A发出的流量就可以均衡发送到CD,当然也可以按其他策略,具体看:

> man ipvsadm

4.3. 其他

F5负载均衡器
kube-proxy

5. 封装