计算机网络报告6

Exp6: DNS报文分析和基于UDP的Socket编程

[TOC]

题解请直接跳转task1,task2,task3

一、实验目的

  • 学习DNS协议
  • 学习使用Datagram Socket

二、实验任务

  • 使用Wireshark分析DNS协议
  • 使用DatagramSocket和DatagramPacket编写代码

三、实验过程

3.1 Wireshark补充

  • 着色规则

数据包列表区中不同的协议使用了不同的颜色区分。协议颜色标识定位在菜单栏View —> Coloring Rules。如下所示:

  • Packet Details Pane(数据包详细信息)

    在数据包列表中选择指定数据包,在数据包详细信息中会显示数据包的所有详细信息内容。数据包详细信息面板是最重要的,用来查看协议中的每一个字段。各行信息分别为:

1
2
3
4
5
1)Frame: 物理层的数据帧概况
2)Ethernet II: 数据链路层以太⽹帧头部信息
3)Internet Protocol Version 4: 互联⽹层IP包头部信息
4)Transmission Control Protocol: 传输层的数据段头部信息,此处是TCP
5)Hypertext Transfer Protocol: 应⽤层的信息,此处是HTTP协议

3.1.1 Wireshake过滤器

  1. 捕获过滤器
    捕获过滤器 是指wireshark⼀开始在抓包时,就确定要抓取哪些类型的包;对于不需要的,不进⾏
    抓取。

  1. 显示过滤器
    显示过滤器 是指wireshark对所有的包都进⾏抓取,当⽤户分析数据包的信息,便于筛选出需要的
    数据包

捕获过滤器 是在⽤户开始任务之前就要使⽤的规则;⽽显示过滤器 是任务开始之后(⽆论是否已
完成)要使⽤的规则。

3.1.2 显示过滤器语法和实例

这⾥我们将主要介绍显示过滤器的常⽤语法和实例,捕获过滤器类似(可百度进⾏了解)。

  1. ⽐较操作符
1
2
3
4
5
6
7
8
9
==(eq)  //等于,equal
!=(ne)  //不等于,no equal
<(lt)   //⼩于,less than
>(gt)   //⼤于,great than
>=(ge)  //⼤于等于,great equal
<=(le)  //⼩于等于,less equal
&&  //逻辑“与”运算
||  //逻辑“或”运算
!   //逻辑“⾮”运算
  1. 协议过滤

⽐较简单,直接在过滤框中直接输⼊协议名即可。 注意:协议名称需要输⼊⼩写。

1
2
3
4
tcp、ip、dhcp、oicq、ftp、ssl等等
icmp //只查看HTTP协议的数据包列表
udp || icmp || dns //只显示udp、icmp、dns相关协议的数据包
not arp 等于 !arp  //不显示arp协议的数据包

  1. 过滤IP地址
1
2
3
4
ip.addr==192.168.1.104  //只显示源/⽬的IP为192.168.1.3的数据包
ip.src==1.1.1.1 //只显示源IP为1.1.1.1的数据包
not ip.src==1.1.1.1   //不显示源IP为1.1.1.1的数据包
ip.src==1.1.1.1 or ip.dst==1.1.1.2  //只显示源IP为1.1.1.1或⽬的IP为1.1.1.2的数据包

  1. 过滤端⼝
1
2
3
4
tcp.port eq 80   //只显示源/⽬的端⼝为80的数据包
tcp.dstport==80  //只显示⽬的端⼝为80的数据包
tcp.srcport==80 //只显示源端⼝为80的数据包
tcp.port >=1 and tcp.port<=80  //只显示源/⽬的端⼝⼤于等于1,⼩于等于80的数据包

  1. 过滤协议参数
1
2
tcp.flags.syn == 0x02  //显示包含syn标志位的数据包
http.request.method=="get"  //显示http请求中method值为get的包

tips:如图所示,在显示过滤器中输⼊规则时,会出现提示信息,可据此了解更多的协议过滤规则

  1. 逻辑运算符为 and/or/not
1
2
过滤多个条件组合时,使⽤and/or。
⽐如获取IP地址为192.168.1.104的ICMP数据包表达式为ip.addr == 192.168.1.104 and icmp

3.2 DNS协议

3.2.1 DNS协议简介

识别主机有两种⽅式:主机名、IP地址。前者便于记忆(如www.baidu.com),但路由器很难处理(主机名⻓度不定);后者定⻓、有层次结构,便于路由器处理,但难以记忆。折中的办法就是建⽴IP地址与主机名间的映射,这就是域名系统DNS做的⼯作。DNS通常由其他应⽤层协议使⽤(如HTTP、SMTP、FTP),将主机名解析为IP地址。
在本实验中,我们将仔细查看 DNS 报⽂的细节。

3.2.2 DNS报⽂

  • 报⽂格式
    DNS只有两种报⽂:查询报⽂、响应报⽂,两者有着相同格式,如下:

  • 捕获的DNS报⽂

考虑访问百度⻚⾯的⼀个操作,在浏览器输⼊http://www.baidu.com/index.html并回⻋,⾸先需要将URL(存放对象的服务器主机名和对象的路径名)解析成IP地址,具体步骤为:

1
2
3
4
5
1)同⼀台⽤户主机上运⾏着DNS应⽤的客户机端(如浏览器)
2)从上述URL抽取主机名[www.baidu.com](http://www.baidu.com/),传给DNS应⽤的客户机端(浏览器)
3)该DNS客户机向DNS服务器发送⼀个包含主机名的请求(DNS查询报⽂)
4)该DNS客户机收到⼀份回答报⽂(DNS响应报⽂),该报⽂包含该主机名对应的IP地址 182.61.200.7
5)浏览器由该IP地址定位的HTTP服务器发送⼀个TCP链接

⽤Wireshark捕获的DNS报⽂如下图,第⼀⾏是DNS查询报⽂,第⼆⾏是DNS响应报⽂。

域名解析过程:

表示授权应答 域名解析总体可分为两大步骤:

第一个步骤是本机向本地域名服务器发出一个DNS请求报文,报文里携带需要查询的域名;第二个步骤是本地域名服务器向本机回应一个DNS响应报文,里面包含域名对应的IP地址或者别名等。

注意:第一个步骤从主机到本地域名服务器是递归查询;第二大步骤中采用的是迭代查询,其实是包含了很多小步骤的

递归查询:本机向本地域名服务器发出一次查询请求,就静待最终的结果。如果本地域名服务器无法解析,自己会以DNS客户机的身份向其它域名服务器查询,直到得到最终的IP地址告诉本机

迭代查询:本地域名服务器向根域名服务器查询,根域名服务器告诉它下一步到哪里去查询,然后它再去查,每次它都是以客户机的身份去各个服务器查询

task1:

内容: 根据Wireshark抓取的报⽂信息(例,下图所示示例),分别分析DNS查询报⽂和响应报⽂的组成结构,参考上⾯的报⽂格式指出报⽂的每个部分(如,头部区域等),请将实验结果附在实验报告中。

DNS 查询报文格式:
报文部分 字段名
头部 Transaction ID (事务ID)
这是DNS报文的ID标识,对于请求报文和其对应的应答报文,该字段的值是相同的。通过这个ID课题区分DNS 应答报文是对哪个请求进行相应的。
头部 Flags (标志)
其中,Flags又有很多子字段,在下面详细介绍。
头部 Questions(问题计数)
头部 Answer RRs(回答资源记录数)
头部 Authority RRs(权威名称能服务器计数)
头部 Additional RRs(附加资源记录数)
问题部分 Queries (查询问题区域)

Flags 这个字段是一串二进制编码,用来标志该请求报文的一些属性和窗台。

1
2
3
4
5
6
7
Flags: 0x0100 Standard query
0... .... .... .... = Response: Message is a query
.000 0... .... .... = Opcode: Standard query (0)
.... ..0. .... .... = Truncated: Message is not truncated
.... ...1 .... .... = Recursion desired: Do query recursively
.... .... .0.. .... = Z: reserved (0)
.... .... ...0 .... = Non-authenticated data: Unacceptable

这个Flags字段里面一共有6个子字段:

  • Response: 查询请求/响应的标志信息。 0代表查询请求,1代表响应请求,这里是0
  • Opcode: 操作码。 0代表标准查询;1代表反响查询; 2代表服务器状态请求。
  • Truncated: 表示是否被截断。 1代表响应已超过512字节并已经被截断,且只返回前面512个字节
  • Recursion Desired: 期望递归。 该字段能在一个查询中设置,并在响应中返回。 该标志告诉名称服务器必须处理这个查询,这种方式被称为一个递归查询。如果该位为 0,且被请求的名称服务器没有一个授权回答,它将返回一个能解答该查询的其他名称服务器列表。这种方式被称为迭代查询。
  • Z: 保留字段,在请求和响应报文中,值必须为0
  • Non-authenticated data: Unacceptable:未经认证的数据;0 代表服务器已经进行了相关 DNSSEC 数字签名的验证 1 代表为服务器并未进行相关 DNSSEC 数字签名的验证

事实上,Flags字段还有其他几个子字段:

  • Authoritative Answer 表示授权应答。 0代表 应答服务器不是该域名的权威解析服务器,1代表应答服务器是该域名的权威解析服务器。
  • rcode返回码字段,表示响应的差错状态。不同的值代表了不同的错误。

Queries 查询问题区域:

该部分是用来显示 DNS 查询请求的问题,通常只有一个问题。该部分包含正在进行的查询信息,包含查询名(被查询主机名字)、查询类型、查询类。

比如说这是一个我查询 CSDN 时候捕捉到的DNS报文中的Queries部分:

1
2
3
4
5
6
7
Queries																		//问题部分
www.csdn.net: type AAAA, class IN
Name: www.csdn.net //被查询的主机名字
[Name Length: 12] //名字长度
[Label Count: 3]
Type: AAAA (IPv6 Address) (28) //查询类型,这里查询的是 IPv6 的地址
Class: IN (0x0001) //查询类字段,这里是互联网地址

注意,DNS响应报文中的 Type类型要和查询报文中的Type类型保持一致。

DNS 响应报文格式:

DNS响应报文的头部、查询问题区域结构基本和响应报文一致。并且一些查询主机的名字、查询类型等信息也需要保持一致。但是比起查询报文,响应报文多了一个资源记录部分:

资源记录部分是指 DNS 报文格式中的最后三个字段,包括Answers (回答问题区域)字段Authoritative nameservers(权威名称服务器区域)字段Additional records(附加信息区域)字段。这三个字段均采用一种称为资源记录的格式。

这里我分析的是访问百度新闻的 DNS 响应报文。

Answers 字段

1
2
3
4
5
6
7
8
Answers
log.news.baidu.com: type CNAME, class IN, cname news.n.shifen.com # 资源记录部分
Name: log.news.baidu.com # 域名字段
Type: CNAME (Canonical NAME for an alias) (5) # 类型字段,这里是CNAME
Class: IN (0x0001) # 类字段
Time to live: 5483 (1 hour, 31 minutes, 23 seconds)# 生存时间
Data length: 16 #数据长度
CNAME: news.n.shifen.com# 资源数据,这里是 CNAME的信息

Authoritative字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Authoritative nameservers
n.shifen.com: type SOA, class IN, mname ns1.n.shifen.com #资源记录部分
Name: n.shifen.com
Type: SOA (Start Of a zone of Authority) (6)# 类型字段,这里是 SOA类型
Class: IN (0x0001) #类字段
Time to live: 234 (3 minutes, 54 seconds)
Data length: 45
Primary name server: ns1.n.shifen.com # 该域名对应的权威名称服务器的名称
Responsible authority's mailbox: baidu_dns_master.baidu.com
Serial Number: 2104090020
Refresh Interval: 5 (5 seconds)
Retry Interval: 5 (5 seconds)
Expire limit: 2592000 (30 days)
Minimum TTL: 3600 (1 hour)

在这里我们要了解一下 类型字段中 CNAME 类型和A类型的区别:

A类型即 Address,也是用来指定主机名(或域名)对应的IP地址记录。用户可以将该域名下的网站服务器指向到自己的web server上。

CNAME类型是 别名记录,也就是说,这种记录允许多个名字映射到另外一个域名。通常用于同时提供WWW和MAIL服务的计算机。例如,有一台计算机名为“host.mydomain.com”(A记录)。它同时提供WWW和MAIL服务,为了便于用户访问服务。可以为该计算机设置两个别名(CNAME):WWW和MAIL。这两个别名的全称就http://www.mydomain.com/mail.mydomain.com。实际上他们都指向 host.mydomain.com

在 Authoritative 字段中的类型字段,这个报文中现实的是 SOA类型,这代表授权起始点 (SOA) 记录会提供有关域和相应托管区域的信息。此外,还有NS记录类型,会标识托管区域的名称服务器。NS 记录的值为名称服务器的域名。

3.3 基于UDP的Socket编程

3.3.1 什么是Socket

  • 简单来说是⼀种地址和端⼝的结合描述协议。
  • TCP/IP协议的相关API的总称、是⽹络API的集合实现,涵盖了TCP和UDP。
  • UDP是⼀种⽤户报协议,⼜称为⽤户数据报⽂协议,是⼀个简单的⾯向数据报的传输层协议。
  • 在本实验中,我们将重点学习基于UDP的Socket编程。

3.3.2 UDP核⼼API

  • DatagramSocket
1
2
3
4
5
6
7
8
⽤于接收与发送UDP的类。
负责发送某⼀个UDP包,或者接收UDP包。
DatagramSocket() //创建简单实例,不指定端⼝与ip
DatagramSocket(int port) //创建监听固定端⼝的实例
DatagramSocket(int port,InetAddress localAddr) //创建固定端⼝指定ip的实例
receive(DatagramPacket d) //接收
send(DatagramPack d) //发送
setSoTimeout(int timeout) //设置超时、毫秒
  • DatagramPacket

⽤于报⽂处理。
将byte数组、⽬标地址、⽬标端⼝等数据包装成报⽂或者将报⽂拆卸成byte数组。
是UDP的发送实体,也是接收实体

1
2
3
4
5
6
7
8
9
10
11
DatagramPacket(byte[] buf,int offset,int length、InetAddress address,int
port)
DatagramPacket(byte[] buf,int offset,int length、SocketAddress address)
setData(byte[] buf ,int offset,int length)
setData(byte[] buf)
setLength(int length)
getData()、getIffset()、getLength()
setAddress(InetAddress iddrr)、setPort(int port)
getAddress()、getPort()
setSocketAddress(SocketAddress address)
getSocketAddress()

3.3.3 小试牛刀: UDP传输案例

UDP不分服务器端和客户端,这⾥为了更好地表示,采⽤了发送者和接收者的说法

task2:

内容:补充完整UDPSearcher类,并运⾏UDPProvider和UDPSearcher,请将实验结果附在实验报告中

首先, TCP是面向连接的,且为两个端系统之间的数据流动提供可靠的字节流通道。而UDP是无连接的,从一个端系统向另一个端系统发送独立的数据分组,不对交付提供任何保证。

这个小程序的功能就是接收我们的学号并返回学号的长度。我将利用多线程来分别启动 UDPProvider和UDPSearcher。

  • 编写UDPProvider类
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
package com.company;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UDPProvider implements Runnable {
@Override
public void run() {
System.out.println("Provider Started");
// 选择⼀个端⼝⽤于数据接收
DatagramSocket datagramSocket = null;
//首先在 9091端口开辟一个 Provider
try {
datagramSocket = new DatagramSocket(9091);
} catch (SocketException e) {
e.printStackTrace();
}
byte[] buf = new byte[1024];
//接收送到这个端口来的数据包
DatagramPacket receivePack = new DatagramPacket(buf, buf.length);
try {
datagramSocket.receive(receivePack);
} catch (IOException e) {
e.printStackTrace();
}
//从数据包中读取发送者的端口号、地址、以及学号的长度
String ip = receivePack.getAddress().getHostAddress();
int port = receivePack.getPort();
int len = receivePack.getLength();
// 将 学号从 Bytes[] 转换成 String 类型
String data = new String(receivePack.getData(), 0, len);
//打印
System.out.println("receive from ip: " + ip + "\tport: " + port +
"\tdata: " + data);
//回送数据
// 编写返回数据的格式
String responseData = "receive data length = " + len;
byte[] responseDataBytes = responseData.getBytes();
//编写返回包的信息
DatagramPacket responsePacket = new DatagramPacket(
responseDataBytes,
responseDataBytes.length,
receivePack.getAddress(),
receivePack.getPort()
);
// 会送数据包
try {
datagramSocket.send(responsePacket);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("Provider finished");
datagramSocket.close();
}
}
  • 编写UDPSearcher
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
package com.company;

import java.io.IOException;
import java.net.*;
import java.util.Arrays;

public class UDPSearcher implements Runnable{
@Override
public void run() {
System.out.println("Searcher Started");
// 搜索⽅不需要指定监听端⼝
DatagramSocket datagramSocket = null;
try {
datagramSocket = new DatagramSocket();
} catch (SocketException e) {
e.printStackTrace();
}
// 发送数据
String sendData = "10195501423";
byte[] sendBytes = sendData.getBytes();
System.out.println(Arrays.toString(sendBytes));
// todo 请补充完sendPack,发送⾄localhost:9091
DatagramPacket sendPack = null;
//DatagramPacket包含四个字段,送出的信息(Bytes[]类型)、送出信息的长度,地址和端口
try {
sendPack = new DatagramPacket(sendBytes,sendBytes.length, InetAddress.getLocalHost(),9091);
} catch (UnknownHostException e) {
e.printStackTrace();
}
try {
datagramSocket.send(sendPack);
} catch (IOException e) {
e.printStackTrace();
}

//接收回送数据
byte[] buf = new byte[1024];
DatagramPacket receivePack = new DatagramPacket(buf, buf.length);
try {
datagramSocket.receive(receivePack);
} catch (IOException e) {
e.printStackTrace();
}
//从接受到的包中提取ip、端口和长度。
String ip = receivePack.getAddress().getHostAddress();
int port = receivePack.getPort();
int len = receivePack.getLength();
String data = new String(receivePack.getData(), 0, len);
System.out.println("receive from ip: " + ip + "\tport: " + port +
"\tdata: " + data);
System.out.println("Searcher finished");
datagramSocket.close();
}
}

main函数

1
2
3
4
5
6
7
8
9
10
11
12
package com.company;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
// 创建两个线程实例,分别调用。
Thread provider = new Thread(new UDPProvider());
Thread sercher = new Thread(new UDPSearcher());

provider.start();
sercher.start();
}
}

结果如下:

task3:

内容:改写UDPProvider和UDPSearcher以完成下述功能,请将实验结果附在实验报告中:

  • ⼴播地址:255.255.255.255
  • 现需完成如下场景的设计:
    • UDPSearcher现将UDP包发送⾄⼴播地址的9091号端⼝(这表示该UDP包将会被⼴播⾄局域⽹下所有主机的对应端⼝)
    • 如果有UDPProvider在监听,解析接受的UDP包,通过解析其中的data得到要回送的端⼝号,并将⾃⼰的⼀些信息写回,UDPSearcher接收到UDPProvider的消息后打印出来。
  • 现提供发送消息的格式:
    • UDPSearcher请使⽤如下buildWithPort构建消息,port在实验中指定为30000
    • UDPProvider请使⽤如下parsePort解析收到的消息并得到要回写的端⼝号,然后⽤buildWithTag构建消息,tag可以是String tag =UUID.randomUUID().toString();,然后回送数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MessageUtil {
private static final String TAG_HEADER = "special tag:";
private static final String PORT_HEADER = "special port:";
public static String buildWithPort(int port) {
return PORT_HEADER + port;
}
public static int parsePort(String data) {
if (data.startsWith(PORT_HEADER)) {
return
Integer.parseInt(data.substring(PORT_HEADER.length()));
}
return -1;
}
public static String buildWithTag(String tag) {
return TAG_HEADER + tag;
}
public static String parseTag(String data) {
if (data.startsWith(TAG_HEADER)) {
return data.substring(TAG_HEADER.length());
}
return null;
}
}

修改过的UDPProvider:

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
package com.company;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.UUID;

public class UDPProvider implements Runnable {
@Override
public void run() {
System.out.println("Provider Started");
// 选择⼀个端⼝⽤于数据接收
DatagramSocket datagramSocket = null;
try {
datagramSocket = new DatagramSocket(9091);
} catch (SocketException e) {
e.printStackTrace();
}
byte[] buf = new byte[1024];
DatagramPacket receivePack = new DatagramPacket(buf, buf.length);
try {
datagramSocket.receive(receivePack);
} catch (IOException e) {
e.printStackTrace();
}
//取得 新的 port
int len = receivePack.getLength();
String newPort = new String(receivePack.getData(), 0, len);
int port = MessageUtil.parsePort(newPort);
System.out.println(port);
String tag =UUID.randomUUID().toString();
String data = MessageUtil.buildWithTag(tag);
//取得 新的ip
String newIP = receivePack.getAddress().getHostAddress();

System.out.println("UDPProvider receive from ip: " + newIP + "\tport: " + port +
"\tdata: " + new String(receivePack.getData(), 0, len));
//回送数据
byte[] responseDataBytes = data.getBytes();
DatagramPacket responsePacket = new DatagramPacket(
responseDataBytes,
responseDataBytes.length,
receivePack.getAddress(),
port
);
try {
datagramSocket.send(responsePacket);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("Provider finished");
datagramSocket.close();
}
}

修改过的UDPSearcher:

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
package com.company;

import java.io.IOException;
import java.net.*;
import java.util.Arrays;

public class UDPSearcher implements Runnable{
@Override
public void run() {
System.out.println("Searcher Started");
// 搜索⽅不需要指定监听端⼝
DatagramSocket datagramSocket = null;
try {
datagramSocket = new DatagramSocket();
} catch (SocketException e) {
e.printStackTrace();
}
// 发送数据
String sendData = MessageUtil.buildWithPort(30000);
byte[] sendBytes = sendData.getBytes();

// todo 请补充完sendPack,发送⾄localhost:9091
DatagramPacket sendPack = null;
try {
sendPack = new DatagramPacket(sendBytes,sendBytes.length,InetAddress.getByName("255.255.255.255"),9091);
datagramSocket.send(sendPack);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

//接收回送数据
try {
datagramSocket = new DatagramSocket(30000);
} catch (SocketException e) {
e.printStackTrace();
}
byte[] buf = new byte[1024];
DatagramPacket receivePack = new DatagramPacket(buf, buf.length);
try {
datagramSocket.receive(receivePack);
} catch (IOException e) {
e.printStackTrace();
}
String ip = receivePack.getAddress().getHostAddress();
int port = receivePack.getPort();
int len = receivePack.getLength();
String data = new String(receivePack.getData(), 0, len);
System.out.println("UDPSearcher receive from ip: " + ip + "\tport: " + port +
"\tdata: " + data);
System.out.println("Searcher finished");
datagramSocket.close();
}
}

结果如下:

-------------本文结束,感谢您的阅读-------------