本文是《Linux高性能服务器编程》第1章的实验,实验机器是我在阿里云的远程主机(服务机)以及本地虚拟机(客户机),系统都是Ubuntu 16.04。使用xshell远程登录服务机,客户机则在VMware Workstation中打开。服务机的安全组设置了8888端口对外开放,服务机以太网网卡接口为eth0(通过ifconfig
查看),公网IP为aliyunserv(具体IP配置于/etc/hosts
中)。客户机以太网网卡接口为ens33,局域网IP为192.168.43.4,并且设置为桥接模式。客户机(虚拟机)的宿主机(windows系统)同一局域网IP为192.168.43.3。
1. 开启服务机的echo服务
首先查看是否支持echo服务。/etc
目录是Linux的配置目录,底下包含各种配置文件,比如密码文件/etc/passwd
,像安装完mysql后也会有个/etc/mysql
目录,底下是mysql的配置文件。这里的/etc/service
文件记录了主机上可能用到的服务及常用端口号/传输层协议。
# cat /etc/services | grep echo
echo 7/tcp
echo 7/udp
at-echo 204/tcp # AppleTalk echo
at-echo 204/udp
echo 4/ddp # AppleTalk Echo Protocol
grep
查找包含某字符串的行,前2行即echo服务,同时支持TCP和UDP。但是要查看系统上是否有该服务,得确认是否在对应端口上有监听套接字,因此用netstat
命令,-l
选项即listening,处于LISTEN(监听)状态的套接字,默认只会显示已连接的套接字。
# netstat -l | grep echo
#
可以看到没有echo进程监听,因此需要主动启用echo服务。
- 安装openbsd-inetd
sudo apt-get install openbsd-inetd
- 使用超级权限打开
/etc/initd.conf
文件,按照注释中的示例添加echo服务(第22行)
16 #:INTERNAL: Internal services
17 #discard stream tcp nowait root internal
18 #discard dgram udp wait root internal
19 #daytime stream tcp nowait root internal
20 #time stream tcp nowait root internal
21
22 echo stream tcp nowait root internal
- 启动服务并查看是否启动成功,这里我一开始用start启动服务,发现没有启动成功,后来改成了restart重启服务才成功的。PS:我这里是root用户,因此不需要加sudo。
# /etc/init.d/openbsd-inetd restart
[ ok ] Restarting openbsd-inetd (via systemctl): openbsd-inetd.service.
# netstat -lp | grep echo
tcp 0 0 *:echo *:* LISTEN
netstat
的-p
选项查看对应进程的PID和名字。端口号被echo替代了,因为echo用的是众所周知的端口号,就像ssh对应端口22,mysql对应端口3306一样。可以用-n
选项查看实际端口号。
2. 客户机telnet远程连接服务机
$ telnet aliyunserv echo
Trying aliyunserv...
telnet: Unable to connect to remote host: Connection refused
从错误提示来看是connect
调用失败,errno
被设置为ECONNREFUSED
。即服务机响应了一个RST,回顾UNPv1的解释,产生RST的三个原因是:
- 目的地为某端口的SYN到达,然而该端口上没有正在监听的服务器;
- TCP想取消一个连接;
- TCP接收到了一个根本不存在的连接上的分节。
从排除法可知是原因2,因为我在安全组上只开启了8888号端口,因此7号端口默认不接受外部连接请求。于是登录阿里云,添加安全组规则如下图所示
终于成功连接echo服务,Ctrl+]退出telnet,如下所示
$ telnet aliyunserv 7
Trying aliyunserv...
Connected to aliyunserv.
Escape character is '^]'.
你好,阿里云!
你好,阿里云!
^]
telnet> q
Connection closed.
telnet
的功能是连接指定IP和端口,然后输入信息回车后发送给对端,接着接收对端的回复。对于echo服务器而言发送和接收的信息是一样的。
3. 连接远程地址
ARP协议实现了任意网络层地址到任意物理地址的转换,比如最常见的,从IPv4地址到MAC地址。(注:下文均用MAC地址替代物理地址,IP地址替代网络层地址)其工作原理是主机向所在的网络广播一个ARP请求,该请求包含目标机器的网络地址,只有被请求的目标机器会回应一个ARP应答。
其中ARP维护一个高速缓存,其中包含经常访问或者最近访问的机器网络地址到物理地址的映射 ,防止每次访问对端都要查询MAC地址,从而减少网络中不必要的流量。通过arp
命令可以查看-a
、添加-s
、删除-d
ARP缓存项。
$ arp -a
? (192.168.43.1) at e6:46:da:f4:f7:5c [ether] on ens33
首先在客户机打开新的终端执行抓包命令,-e
选项开启以太网帧头部信息的显示,-n
选项使用IP地址(而非主机名)代表主机,-t
选项关闭时间戳的显示。
$ sudo tcpdump arp -ent
然后在原来的终端上删除该缓存项后用telnet
连接到服务机
$ arp -a
? (192.168.43.1) at <incomplete> on ens33
$ telnet aliyunserv echo
Trying aliyunserv...
Connected to aliyunserv.
Escape character is '^]'.
抓包结果如下
00:0c:29:b0:61:05 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Request who-has 192.168.43.1 tell 192.168.43.4, length 28
e6:46:da:f4:f7:5c > 00:0c:29:b0:61:05, ethertype ARP (0x0806), length 60: Reply 192.168.43.1 is-at e6:46:da:f4:f7:5c, length 46
第1个包目的MAC地址是广播地址ff:ff:ff:ff:ff:ff,表示整个局域网(LAN),该局域网上所有机器都会收到并处理这样的帧。0x0806表示这是ARP协议。由于以太网头部是14字节,所以前一个length比后一个length大14。
第1个包为Request(请求包),第2个包为Reply(回复包)。
192.168.43.1为路由器地址,因为所访问的远程主机并不在局域网内,所以找到的MAC地址为路由器地址,由路由器来完成后续寻址。
4. 连接局域网内地址
tcpdump抓包命令不变,这次telnet连接宿主机(桥接模式下相当于和客户机在同一个局域网内)。这次不折腾echo服务了,回顾之前的流程,本质是服务器启动监听socket,客户端connect连接服务器,connect伴随着三次握手,握手需要IP寻址,进而需要知道MAC地址。
所以这次直接在宿主机上用python进行监听,打开IDLE窗口,输入脚本如下
>>> import socket
>>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> sock.bind(('0.0.0.0', 8888))
>>> sock.listen()
>>> conn, addr = sock.accept()
此时accept陷入阻塞,等待客户机连接。还是在一个终端窗口中开始抓包,tcpdump命令同上。然后在另一个窗口中用telnet连接
$ telnet 192.168.43.3 8888
Trying 192.168.43.3...
Connected to 192.168.43.3.
Escape character is '^]'.
telnet [IP] [端口]等价于调用socket
创建TCP套接字,然后调用connect
连接对应IP和端口的地址。抓包结果如下
00:0c:29:b0:61:05 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Request who-has 192.168.43.3 tell 192.168.43.4, length 28
28:e3:47:45:ba:cd > 00:0c:29:b0:61:05, ethertype ARP (0x0806), length 60: Reply 192.168.43.3 is-at 28:e3:47:45:ba:cd, length 46
这次可以看到,第二个数据包(ARP回复包)的源IP是192.168.43.3,即宿主机地址,而非路由器地址。再查看ARP缓存会发现多了一项
$ arp -a
? (192.168.43.1) at e6:46:da:f4:f7:5c [ether] on ens33
IQ8OX37RVFOQ4I9 (192.168.43.3) at 28:e3:47:45:ba:cd [ether] on ens33
局域网路由器的MAC地址为e6:46:da:f4:f7:5c,另一台主机的MAC地址为28:e3:47:45:ba:cd。
5. 小结
从上述抓包过程中可以发现,ARP协议获取的并不是目的主机的真实MAC地址,只有在目的主机在同一个局域网内时才能获取其真实MAC地址,否则获取的是路由器的MAC地址。
从这里也可以得出IP地址和MAC地址缺一不可的原因。
首先,若没有IP地址,由于ARP协议是通过广播的形式来查找MAC地址的,如果在整个广域网内用广播的方式来查找,几乎等同于大海捞针。只有在结合IP地址后,才能结合相关寻址算法来找寻源主机到目的主机所经过的路由器组成的路径。同时,ARP缓存只需要记录局域网内的主机MAC地址即可,如果广域网的主机MAC地址也记下来,内存很容易就爆掉。
另一方面,IP首部会记录源IP和目的IP,但是仅靠这两个信息是不知道路径上下一跳路由器的地址的,因此会用MAC地址来记录下一跳路由器的地址。
因此,对于一个发往外网地址的网络包,以太首部+IP首部+数据(传输层、应用层),以太首部记录下一跳路由器的MAC地址,IP首部记录目的地址的IP。虽然这个数据包在物理上是发给路由器的,但是逻辑上是发给目的主机的,前者用物理地址表示,后者用网络地址表示。
其他参考资料
[1] ubuntu 14.04中打开echo、daytime等标准服务详细实例图解
[2] What Is /etc/services in Linux/Unix?
[3] 《Linux高性能服务器编程》
[4] 《Unix网络编程 卷一:套接字联网API》第3版
[5] 《图解TCP/IP》第5版