计算机网络实验——链路层、网络层

说明

实验主要来源于Rechard Stevens 的《TCP/IP illustrated》(《TCP/IP 详解》)卷一和刘超老师的极客时间专栏《趣谈网络协议》,实验网络拓扑基于docker搭建,github地址戳这里

网络拓扑如下图所示:

具体的搭建过程可以看刘超老师写的一篇博文:有了Openvswitch和Docker,终于可以做《TCP/IP详解》的实验了!

脚本基本上是一键安装运行的,但是有一点要注意,就是下面的命令中,./setupenv.sh后面一定要跟你自己机器的网络接口(否则可能docker容器访问不了外网),例如我的机器是eth1,那么就是./setupenv.sh eth1 hub.c.163.com/liuchao110119163/ubuntu:tcpip

1
2
3
4
5
git clone https://github.com/popsuper1982/tcpipillustrated.git
cd tcpipillustrated
docker pull hub.c.163.com/liuchao110119163/ubuntu:tcpip
chmod +x setupenv.sh
./setupenv.sh enp0s3 hub.c.163.com/liuchao110119163/ubuntu:tcpip

如果还有问题欢迎留言交流。

工具介绍

docker

tcpdump

scapy

安装

由于实验机是Ubuntu 14.04,需要使用下列方式安装python3:

1
2
3
4
5
6
7
8
9
10
11
# If you are using Ubuntu 16.10 or newer, then you can easily install Python 3.6 with the following commands:

$ sudo apt-get update
$ sudo apt-get install python3.6

# If you’re using another version of Ubuntu (e.g. the latest LTS release), we recommend using the deadsnakes PPA to install Python 3.6:

$ sudo apt-get install software-properties-common
$ sudo add-apt-repository ppa:deadsnakes/ppa
$ sudo apt-get update
$ sudo apt-get install python3.6

安装pip:

Installing with get-pip.py
To install pip, securely download get-pip.py:

curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
Then run the following:

python get-pip.py
Warning Be cautious if you are using a Python install that is managed by your operating system or another package manager. get-pip.py does not coordinate with those tools, and may leave your system in an inconsistent state.

安装scapy:

pip3 install scapy

实验

ARP实验

先来简单回顾一下ARP协议,它的全称是Address Resolution Protocol,中文名地址解析协议,作用是进行IP地址到MAC地址的映射。过程可以看下面两张图:

ARP是基于以太网帧进行封装的,关于ARP协议具体工作与哪一层还有争议,网上一篇博文这个说法我认为比较合理:

可以这样说,在OSI模型中ARP协议属于链路层;而在TCP/IP模型中,ARP协议属于网络层。

不过除了考试做题,这个争议没用什么意义。比较关键的是以太网帧的结构,如下图所示:

ARP包在以太网帧的基础上进行封装,包结构如下图所示:

有了上面的基础,现在可以开始做实验了。

ARPPing实验

类别 主机名 接口地址
源主机 bsdi 140.242.13.35
目的主机 sun 140.252.13.33

使用scapy可以很简单的进行ARPPing:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# bsdi
# `srp` means send and receive packet
# ethernet frame `dst` field is broadcast
# arp packet encapsulated in ethernet frame data
# send arp packet from bsdi to sun
>>> ans, unans = srp(Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst="140.252.13.33"))
Begin emission:
*Finished sending 1 packets.

Received 1 packets, got 1 answers, remaining 0 packets

# inspect ans
>>> ans
<Results: TCP:0 UDP:0 ICMP:0 Other:1>

# from the output of srp we can see that
# a packet was sent and a packet was received
# see ans.summary()
# it was the request and response of a arp process
>>> ans.summary()
Ether / ARP who has 140.252.13.33 says 140.252.13.35 ==> Ether / ARP is at 86:43:ab:ea:6e:96 says 140.252.13.33

# see the content of the ans tuple
# ans[0][0] is the arp request
# ans[0][1] is the arp reply
>>> ans[0]
(<Ether dst=ff:ff:ff:ff:ff:ff type=0x806 |<ARP pdst=140.252.13.33 |>>,
<Ether dst=02:1c:55:0e:bb:87 src=86:43:ab:ea:6e:96 type=0x806 |<ARP hwtype=0x1 ptype=0x800 hwlen=6 plen=4 op=is-at hwsrc=86:43:ab:ea:6e:96 psrc=140.252.13.33 hwdst=02:1c:55:0e:bb:87 pdst=140.252.13.35 |>>)

# see the request packet format
# the `dst` field is a broadcast address
# notice that the `type` field is 0x0806
# which means arp
# hwtype(hardware type) value 1 means ethernet
# ptype(protocol type) value 0x0800 means IP
# hwlen should be 6(length of MAC address)
# plen should be 4(length of IP addres)
# op `who-has` means request opreation
>>> ans[0][0].show()
###[ Ethernet ]###
dst= ff:ff:ff:ff:ff:ff
src= 02:1c:55:0e:bb:87
type= 0x806
###[ ARP ]###
hwtype= 0x1
ptype= 0x800
hwlen= None
plen= None
op= who-has
hwsrc= 02:1c:55:0e:bb:87
psrc= 140.252.13.35
hwdst= None
pdst= 140.252.13.33

# see the reply packet format
# its `dst` field is the `src' filed of request packet
# `type` is also 0x806
# op `is-at` means arp reply
>>> ans[0][1].show()
###[ Ethernet ]###
dst= 02:1c:55:0e:bb:87
src= 86:43:ab:ea:6e:96
type= 0x806
###[ ARP ]###
hwtype= 0x1
ptype= 0x800
hwlen= 6
plen= 4
op= is-at
hwsrc= 86:43:ab:ea:6e:96
psrc= 140.252.13.33
hwdst= 02:1c:55:0e:bb:87
pdst= 140.252.13.35

使用tcpdump在sun端进行抓包:

1
2
3
4
5
6
7
8
9
10
11
12
13
# -i select interface
# -n do not resolve IP address
root@70aa710235b6:$ tcpdump -i eth1 -n
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), capture size 262144 bytes
00:35:44.116294 ARP, Request who-has 140.252.13.33 tell 140.252.13.35, length 42
00:35:44.116317 ARP, Reply 140.252.13.33 is-at 86:43:ab:ea:6e:96, length 28

# add -vv
root@70aa710235b6:$ tcpdump -i eth1 -n -vv
tcpdump: listening on eth1, link-type EN10MB (Ethernet), capture size 262144 bytes
00:50:14.050065 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 140.252.13.33 tell 140.252.13.35, length 42
00:50:14.050087 ARP, Ethernet (len 6), IPv4 (len 4), Reply 140.252.13.33 is-at 86:43:ab:ea:6e:96, length 28

本实验使用scapy构造并发送了一个简单的ARP包,并在目标主机使用tcpdump进行了抓包操作。主要是为了对ARP协议的功能和包结构有一个直观的认识。完整的ARPPing实际可以一次性对一个网段内的所有主机进行进行Ping测试,scapy代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# arp ping to subnet 140.252.13.32-63
# net 140.252.13.32
# mask 255.255.255.224
>>> ans, unans = srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="140.252.13.32/27"),
timeout=2)
Begin emission:
**Finished sending 32 packets.

Received 2 packets, got 2 answers, remaining 30 packets

>>> ans.summary()
Ether / ARP who has 140.252.13.33 says 140.252.13.35 ==> Ether / ARP is at 86:43:ab:ea:6e:96 says 140.252.13.33
Ether / ARP who has 140.252.13.34 says 140.252.13.35 ==> Ether / ARP is at 22:8a:5c:68:86:ec says 140.252.13.34

结合实验拓扑可以看出结果符合预期。子网140.252.13.32/27可以表示为140.252.13.001 00000,bsdi主机的一个网卡接口的IP地址为140.252.13.001 00011,sun主机的一个网卡接口的IP地址140.252.13.33/27表示为140.252.13.001 00001,svr4主机的一个网卡接口的IP地址可以表示为140.252.13.001 00010,根据子网划分的规则可以看出bsdi、sun和svr4都属于同一个子网。而主机slip的网卡接口的IP地址表示为140.252.13.010 00001,不属于子网140.252.13.001 00000。因此从bsdi发起的ARPPing应该能收到sun和svr4的ARP响应,但是收不到slip的ARP响应。

140.252.13.32/27 140.252.13.001 00000
bsdi 140.252.32.35/27 140.242.13.001 00011
sun 140.252.32.33/27 140.252.13.001 00001
svr4 140.252.32.34/27 140.252.13.001 00010
slip 140.252.32.65/27 140.252.13.010 00001

使用telnet连接无效服务器

类别 主机名 接口地址
源主机 bsdi 140.242.13.35
目的主机 svr4 140.252.13.34

从bsdi向svr4发起telnet连接,再svr4使用tcpdump抓包。

1
2
3
4
5
6
# bsdi
root@d70675fa03bc:$ telnet 140.252.13.34 22
Trying 140.252.13.34...
Connected to 140.252.13.34.
Escape character is '^]'.
SSH-2.0-OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.6
1
2
3
4
5
6
7
# svr4
root@6b628ae5483f:$ tcpdump -i eth1 -n -vv arp
tcpdump: listening on eth1, link-type EN10MB (Ethernet), capture size 262144 bytes
12:38:25.752415 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 140.252.13.34 tell 140.252.13.35, length 28
12:38:25.752433 ARP, Ethernet (len 6), IPv4 (len 4), Reply 140.252.13.34 is-at 22:8a:5c:68:86:ec, length 28
12:38:30.763321 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 140.252.13.35 tell 140.252.13.34, length 28
12:38:30.763927 ARP, Ethernet (len 6), IPv4 (len 4), Reply 140.252.13.35 is-at 02:1c:55:0e:bb:87, length 28

本实验说明再进行上层通信前需要先获取局域网内主机的MAC地址。

查看一下bsdi的ARP缓存,可以看到bsdi已经将svr4的MAC地址加入了缓存中。

1
2
3
4
5
root@d70675fa03bc:$ arp -n
Address HWtype HWaddress Flags Mask Iface
140.252.13.34 ether 22:8a:5c:68:86:ec C eth1
140.252.13.65 (incomplete) bsdiside
140.252.13.33 (incomplete) eth1

使用telnet连接不存在主机

类别 主机名 接口地址
源主机 bsdi 140.242.13.35
目的主机 ???? 140.252.13.36

注意目的主机不存在,根据网络号和子网号所对应的网络确实存在,但是并不存在所指定的主机号。从图1可以看出,主机号从36到62的主机并不存在(主机号为63是广播地址)。这里用主机号36来举例。使用一个shell发起telnet连接:

1
2
3
4
5
6
# bsdi
root@d70675fa03bc:$ date; telnet 140.252.13.36;date
Fri Mar 29 12:51:34 UTC 2019
Trying 140.252.13.36...
telnet: Unable to connect to remote host: No route to host
Fri Mar 29 12:51:37 UTC 2019

另一个shell使用tcpdump进行抓包:

1
2
3
4
5
6
# bsdi
root@d70675fa03bc:$ tcpdump -i eth1 -n -vv
tcpdump: listening on eth1, link-type EN10MB (Ethernet), capture size 262144 bytes
12:51:34.301832 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 140.252.13.36 tell 140.252.13.35, length 28
12:51:35.299308 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 140.252.13.36 tell 140.252.13.35, length 28
12:51:36.299308 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 140.252.13.36 tell 140.252.13.35, length 28

可以看出这里ARP请求重发了三次,说明ARP请求是有重传机制的,重传的间隔约为1s。

注意,在线路上始终看不到TCP的报文段。我们能看到的是ARP请求。直到ARP回答返回时,TCP报文段才可以被发送,因为硬件地址到这时才可能知道。如果我们用过滤模式运行tcpdump命令,只查看TCP数据,那么将没有任何输出。

ARP代理

ARP代理也称作混合ARP(promiscuousARP)或ARP出租(ARP hack)。这些名字来自于ARP代理的其他用途:通过两个物理网络之间的路由器可以互相隐藏物理网络。在这种情况下,两个物理网络可以使用相同的网络号,只要把中间的路由器设置成一个ARP代理,以响应一个网络到另一个网络主机的ARP请求。这种技术在过去用来隐藏一组在不同物理电缆上运行旧版TCP/IP的主机。分开这些旧主机有两个共同的理由,其一是它们不能处理子网划分,其二是它们使用旧的广播地址(所有比特值为0的主机号,而不是目前使用的所有比特值为1 的主机号)。
如果ARP请求是从一个网络的主机发往另一个网络上的主机,那么连接这两个网络的路由器就可以回答该请求,这个过程称作委托ARP或ARP代理(Proxy ARP)。这样可以欺骗发起ARP请求的发送端,使它误以为路由器就是目的主机,而事实上目的主机是在路由器的“另一边”。路由器的功能相当于目的主机的代理,把分组从其他主机转发给它。

类别 主机名 接口地址
源主机 gemini 140.252.1.11
目的主机 sun 140.242.13.33

实验拓扑如下图所示:

如图1所示,系统sun与两个以太网相连。但是事实上并不是这样,在sun和子网140.252.1之间实际存在一个路由器,就是这个具有ARP代理功能的路由器使得sun就好像在子网140.252.1上一样。具体安置如图7所示,路由器Telebit NetBlazer,取名为netb,在子网和主机sun之间。

当子网140.252.1(称作gemini)上的其他主机有一份IP数据报要传给地址为140.252.1.29的sun时,gemini比较网络号(140.252)和子网号(1),因为它们都是相同的,因而在图7上面的以太网中发送IP地址140.252.1.29的ARP请求。路由器netb识别出该IP地址属于它的一个拔号主机,于是把它的以太网接口地址140.252.1作为硬件地址来回答。主机gemini通过以太网发送IP数据报到netb,netb通过拨号SLIP链路把数据报转发到sun。这个过程对于所有140.252.1子网上的主机来说都是透明的,主机sun实际上是在路由器netb后面进行配置的。

从主机gemini向主机sun发起ssh连接,分别在gemini(右上)、netb(左下)和sun(右下)进行抓包。

完成操作后查看gemini的ARP缓存:

1
2
3
4
5
root@07928580bdcd:$ arp -n
Address HWtype HWaddress Flags Mask Iface
140.252.1.29 ether 32:3a:6f:cd:a7:24 C eth1
140.252.1.4 ether c6:74:e6:3b:dd:e6 C eth1
140.252.1.183 ether 32:3a:6f:cd:a7:24 C eth1

经过与主机sun通信以后,发现在同一个子网140.252.1上的netb和sun的IP地址映射的硬件地址是相同的。这通常是使用委托ARP的线索。netb作为ARP代理,使得sun就像是子网140.252.1的一部分一样。

IP实验

bsdi向sun发送数据包

类别 主机名 接口地址
源主机 bsdi 140.242.13.35
目的主机 sun 140.252.13.33

在主机bsdi使用scapy构建IP数据包,并发送到sun:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# bsdi
>>> packet = IP(dst="140.252.13.33")
>>> packet.show()
###[ IP ]###
version= 4
ihl= None
tos= 0x0
len= None
id= 1
flags=
frag= 0
ttl= 64
proto= hopopt
chksum= None
src= 140.252.13.35
dst= 140.252.13.33
\options\

>>> send(packet)

对比scapy中打印的结果和图9中IP包的结构可以看出构造的IP包可以不用填写每一个字段,也就是说可以使用scapy构造不完整甚至有问题的IP数据包在网络中传输。

在sun使用tcpdump进行抓包:

1
2
3
4
5
6
7
8
# sun
# -i 用于指定接口
# -nn 不进行IP到域名和服务到端口的转换
root@70aa710235b6:$ tcpdump -i eth1 -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), capture size 262144 bytes
14:10:19.512733 IP 140.252.13.35 > 140.252.13.33: ip-proto-0 0
14:10:19.512771 IP 140.252.13.33 > 140.252.13.35: ICMP 140.252.13.33 protocol 0 unreachable,length 28

由于这个IP包没有特定的接收目标,sun端返回了一个协议不可达的ICMP报文。在bsdi端抓取该ICMP报文:

1
2
3
4
5
6
7
8
9
10
11
12
13
# bsdi
>>> res = sniff(timeout=5)
>>> res.summary()
Ether / ARP who has 140.252.13.33 says 140.252.13.35 / Padding
Ether / ARP is at 86:43:ab:ea:6e:96 says 140.252.13.33
Ether / 140.252.13.35 > 140.252.13.33 hopopt / Padding
Ether / IP / ICMP 140.252.13.33 > 140.252.13.35 dest-unreach protocol-unreachable / IPerror

>>> res[ICMP][0]
<Ether dst=02:1c:55:0e:bb:87 src=86:43:ab:ea:6e:96 type=0x800 |
<IP version=4 ihl=5 tos=0xc0 len=48 id=25344 flags= frag=0 ttl=64 proto=icmp chksum=0xe2d0 src=140.252.13.33 dst=140.252.13.35 |
<ICMP type=dest-unreach code=protocol-unreachable chksum=0xfcfd reserved=0 length=0 nexthopmtu=0 |
<IPerror version=4 ihl=5 tos=0x0 len=20 id=1 flags= frag=0 ttl=64 proto=hopopt chksum=0x46ad src=140.252.13.35 dst=140.252.13.33 |>>>>

关于ICMP的详细信息将在ICMP实验中进行介绍。

IP路由选择的实验

主机选路时的优先级:

  1. 搜索匹配的主机地址;
  2. 搜索匹配的网络地址;
  3. 搜索默认表项(默认表项一般在路由表中被指定为一个网络表项,其网络号为0)。

TODO: 这部分内容暂时做不了,但是是很有意义的实验,值得研究。

ICMP实验

ICMP全称Internet Control Message Protocol,就是互联网控制报文协议。ICMP报文是封装在IP包里面的,它本身非常简单,通常被IP层或更高层的协议使用(UDP和TCP)。ICMP主要分为查询报文和查询报文,其格式如下图所示:

ICMP地址掩码请求与应答

ICMP地址掩码请求用于无盘系统在引导过程中获取自己的子网掩码。

实验过程中发现没有主机会响应该ICMP请求报文,搜索后发现stackexchange上的一个答案说该ICMP查询请求已被废弃:

RFC 6918 deprecates several ICMP types:

Alternate Host Address (Type 6)
Information Request (Type 15)
Information Reply (Type 16)
Address Mask Request (Type 17)
Address Mask Reply (Type 18)
Traceroute (Type 30)
Datagram Conversion Error (Type 31)
Mobile Host Redirect (Type 32)
IPv6 Where-Are-You (Type 33)
IPv6 I-Am-Here (Type 34)
Mobile Registration Request (Type 35)
Mobile Registration Reply (Type 36)
Domain Name Request (Type 37)
Domain Name Reply (Type 38)
SKIP (Type 39)
Not a straight answer but probably the best you can get.

ICMP时间戳请求与应答

报文请求结构如图所示:

类别 主机名 接口地址
源主机 bsdi 140.242.13.35
目的主机 svr4 140.252.13.34
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# bsdi
# define a function to get current time
>>> def getnow():
...: yy, mm, dd, *_ = time.localtime()
...: return int((time.time() - time.mktime((yy, mm, dd, 0, 0, 0, 0, 0, 0))) * 1000)

# send and receive icmp timestamp request and reply
>>> ans, unans = sr(IP(dst="140.252.13.34")/ICMP(type=13, ts_ori=getnow()))
Begin emission:
*Finished sending 1 packets.

Received 1 packets, got 1 answers, remaining 0 packets

>>> ans
<Results: TCP:0 UDP:0 ICMP:1 Other:0>

>>> ans.summary()
IP / ICMP 140.252.13.35 > 140.252.13.34 timestamp-request 0 ==> IP / ICMP 140.252.13.34 > 140.252.13.35 timestamp-reply 0

>>> ans[0][0].show()
###[ IP ]###
version= 4
ihl= None
tos= 0x0
len= None
id= 1
flags=
frag= 0
ttl= 64
proto= icmp
chksum= None
src= 140.252.13.35
dst= 140.252.13.34
\options\
###[ ICMP ]###
type= timestamp-request
code= 0
chksum= None
id= 0x0
seq= 0x0
ts_ori= 0:52:2.190
ts_rx= 15:0:25.317
ts_tx= 15:0:25.317

>>> ans[0][1].show()
###[ IP ]###
version= 4
ihl= 5
tos= 0x0
len= 40
id= 26909
flags=
frag= 0
ttl= 64
proto= icmp
chksum= 0xdd7a
src= 140.252.13.34
dst= 140.252.13.35
\options\
###[ ICMP ]###
type= timestamp-reply
code= 0
chksum= 0x541
id= 0x0
seq= 0x0
ts_ori= 0:52:2.190
ts_rx= 0:52:2.193
ts_tx= 0:52:2.193

使用ICMP向NTP服务器请求时间戳:

国内常用NTP服务器地址及IP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# bsdi
# type 13/14 for request/response
>>> ans, unans = sr(IP(dst="cn.pool.ntp.org")/ICMP(type=13, ts_ori=getnow()))
Begin emission:
Finished sending 1 packets.

Received 1 packets, got 1 answers, remaining 0 packets

>>> ans.summary()
IP / ICMP 140.252.13.35 > 119.28.183.184 timestamp-request 0 ==> IP / ICMP 119.28.183.184 >
140.252.13.35 timestamp-reply 0

>>> ans[0][0].show()
###[ IP ]###
version= 4
ihl= None
tos= 0x0
len= None
id= 1
flags=
frag= 0
ttl= 64
proto= icmp
chksum= None
src= 140.252.13.35
dst= 119.28.183.184
\options\
###[ ICMP ]###
type= timestamp-request
code= 0
chksum= None
id= 0x0
seq= 0x0
ts_ori= 12:36:41.294
ts_rx=
ts_tx=

>>> ans[0][1].show()
###[ IP ]###
version= 4
ihl= 5
tos= 0x68
len= 40
id= 30896
flags=
frag= 0
ttl= 43
proto= icmp
chksum= 0x4dc9
src= 119.28.183.184
dst= 140.252.13.35
\options\
###[ ICMP ]###
type= timestamp-reply
code= 0
chksum= 0x9b2f
id= 0x0
seq= 0x0
ts_ori= 12:36:41.294
ts_rx= 12:36:41.330
ts_tx= 12:36:41.330

在bsdi使用tcpdump抓包:

1
2
3
4
5
6
root@d70675fa03bc:$ tcpdump -i eth1 -vv icmp
tcpdump: listening on eth1, link-type EN10MB (Ethernet), capture size 262144 bytes
12:36:41.297617 IP (tos 0x0, ttl 64, id 1, offset 0, flags [none], proto ICMP (1), length 40)
140.252.13.35 > 119.28.183.184: ICMP time stamp query id 0 seq 0, length 20
12:36:41.334079 IP (tos 0x68, ttl 43, id 30896, offset 0, flags [none], proto ICMP (1), length 40)
119.28.183.184 > 140.252.13.35: ICMP time stamp reply id 0 seq 0: org 12:36:41.294, recv 12:36:41.330, xmit 12:36:41.330, length 20

ICMP端口不可达报文

它是ICMP目的不可到达报文中的一种,本实验通过人为制造的无效UDP报文查看ICMP差错报文中所附加的信息。

UDP的规则之一是,如果收到一份UDP数据报而目的端口与某个正在使用的进程不相符,那么UDP返回一个ICMP不可达报文。

报文请求结构如下图所示:

类别 主机名 接口地址
源主机 bsdi 140.242.13.35
目的主机 svr4 140.252.13.34
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# bsdi
root@d70675fa03bc:$ tftp
tftp> connect 140.252.13.34 8888
tftp> get sth
Transfer timed out.

root@d70675fa03bc:$ tcpdump -i eth1 -nn -v
tcpdump: listening on eth1, link-type EN10MB (Ethernet), capture size 262144 bytes
13:08:04.411673 02:1c:55:0e:bb:87 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 140.252.13.34 tell 140.252.13.35, length 28
13:08:04.412015 22:8a:5c:68:86:ec > 02:1c:55:0e:bb:87, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Reply 140.252.13.34 is-at 22:8a:5c:68:86:ec, length 28
13:08:04.412023 02:1c:55:0e:bb:87 > 22:8a:5c:68:86:ec, ethertype IPv4 (0x0800), length 57: (tos 0x0, ttl 64, id 63030, offset 0, flags [DF], proto UDP (17), length 43)
140.252.13.35.50224 > 140.252.13.34.8888: UDP, length 15
13:08:04.413318 22:8a:5c:68:86:ec > 02:1c:55:0e:bb:87, ethertype IPv4 (0x0800), length 85: (tos 0xc0, ttl 64, id 2041, offset 0, flags [none], proto ICMP (1), length 71)
140.252.13.34 > 140.252.13.35: ICMP 140.252.13.34 udp port 8888 unreachable, length 51
(tos 0x0, ttl 64, id 63030, offset 0, flags [DF], proto UDP (17), length 43)
140.252.13.35.50224 > 140.252.13.34.8888: UDP, length 15
13:08:09.411913 02:1c:55:0e:bb:87 > 22:8a:5c:68:86:ec, ethertype IPv4 (0x0800), length 57: (tos 0x0, ttl 64, id 64140, offset 0, flags [DF], proto UDP (17), length 43)
140.252.13.35.50224 > 140.252.13.34.8888: UDP, length 15
13:08:09.411979 22:8a:5c:68:86:ec > 02:1c:55:0e:bb:87, ethertype IPv4 (0x0800), length 85: (tos 0xc0, ttl 64, id 2194, offset 0, flags [none], proto ICMP (1), length 71)
140.252.13.34 > 140.252.13.35: ICMP 140.252.13.34 udp port 8888 unreachable, length 51
(tos 0x0, ttl 64, id 64140, offset 0, flags [DF], proto UDP (17), length 43)
140.252.13.35.50224 > 140.252.13.34.8888: UDP, length 15

# use scapy sniff ICMP port unreachable packet
>>> res = sniff()

>>> res
<Sniffed: TCP:0 UDP:5 ICMP:5 Other:4>

>>> res[ICMP].summary()
Ether / IP / ICMP 140.252.13.34 > 140.252.13.35 dest-unreach port-unreachable / IPerror / UDPerror / Raw
Ether / IP / ICMP 140.252.13.34 > 140.252.13.35 dest-unreach port-unreachable / IPerror / UDPerror / Raw
Ether / IP / ICMP 140.252.13.34 > 140.252.13.35 dest-unreach port-unreachable / IPerror / UDPerror / Raw
Ether / IP / ICMP 140.252.13.34 > 140.252.13.35 dest-unreach port-unreachable / IPerror / UDPerror / Raw
Ether / IP / ICMP 140.252.13.34 > 140.252.13.35 dest-unreach port-unreachable / IPerror / UDPerror / Raw

>>> res[ICMP][0][0].show()
###[ Ethernet ]###
dst= 02:1c:55:0e:bb:87
src= 22:8a:5c:68:86:ec
type= 0x800
###[ IP ]###
version= 4
ihl= 5
tos= 0xc0
len= 71
id= 46842
flags=
frag= 0
ttl= 64
proto= icmp
chksum= 0x8ebe
src= 140.252.13.34
dst= 140.252.13.35
\options\
###[ ICMP ]###
type= dest-unreach
code= port-unreachable
chksum= 0x3163
reserved= 0
length= 0
nexthopmtu= 0
###[ IP in ICMP ]###
version= 4
ihl= 5
tos= 0x0
len= 43
id= 20498
flags= DF
frag= 0
ttl= 64
proto= udp
chksum= 0xb672
src= 140.252.13.35
dst= 140.252.13.34
\options\
###[ UDP in ICMP ]###
sport= 38693
dport= 8888
len= 23
chksum= 0x7ca0
###[ Raw ]###
load= '\x00\x01foo\x00netascii\x00'

此处使用了TFTP协议强制生成了一个端口不可达报文,connect命令首先指定要连接的主机名及其端口号,接着用get命令来取文件。在UDP数据报送到svr4之前,要先发送一份ARP请求来确定它的硬件地址,接着返回ARP应答(tcpdump抓取到的前两个数据包),然后才发送UDP数据包,一个ICMP端口不可达差错是立刻返回的。接着又尝试了几次后客户程序才放弃,打印了一个超时的结果。

ICMP的一个规则是,ICMP差错报文必须包括生成该差错报文的数据报IP首部(包含任何选项),还必须至少包括跟在该IP首部后面的前8个字节。在我们的例子中,跟在IP首部后面的前8个字节包含UDP的首部

这一点可以从scapy的抓包结果中看出。

一个重要的事实是包含在UDP首部中的内容是源端口号和目的端口号。就是由于目的端口号(8888)才导致产生了ICMP端口不可达的差错报文。接收ICMP的系统可以根据源端口号来把差错报文与某个特定的用户进程相关联(在本例中是TFTP客户程序)。

导致差错的数据报中的IP首部要被送回的原因是因为IP首部中包含了协议字段,使得ICMP可以知道如何解释后面的8个字节(在本例中是UDP首部)。如果查看TCP首部,可以发现源端口和目的端口被包含在TCP首部的前8个字节中

ICMP协议不可达报文

这个实验同IP层的bsdi向sun发送数据包实验,此处只研究返回的ICMP报文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
>>> res[ICMP][0].show()
###[ Ethernet ]###
dst= 02:1c:55:0e:bb:87
src= 86:43:ab:ea:6e:96
type= 0x800
###[ IP ]###
version= 4
ihl= 5
tos= 0xc0
len= 48
id= 25344
flags=
frag= 0
ttl= 64
proto= icmp
chksum= 0xe2d0
src= 140.252.13.33
dst= 140.252.13.35
\options\
###[ ICMP ]###
type= dest-unreach
code= protocol-unreachable
chksum= 0xfcfd
reserved= 0
length= 0
nexthopmtu= 0
###[ IP in ICMP ]###
version= 4
ihl= 5
tos= 0x0
len= 20
id= 1
flags=
frag= 0
ttl= 64
proto= hopopt
chksum= 0x46ad
src= 140.252.13.35
dst= 140.252.13.33
\options\

因为这个错误是网络层的错误,没有和特定的进程产生关联,所以ICMP报文中只有出错IP报文的信息,而不像上一个实验中既有网络层的出错信息,也有传输层的出错信息。

小结

通过使用docker搭建的实验环境,我尝试复现一些《TCP/IP 详解》卷一中的例子,并将这些例子的构造方法和网络流信息进行记录,主要是数据链路层和网络层的例子。并在进行实验的过程中熟悉了scapy和tcpdump等工具的一些基本用法。

引用

《TCP/IP 详解》卷一
The Hitchhiker’s Guide to Python!