网易首页 > 网易号 > 正文 申请入驻

从零开始,运用 Ruby 语言创建一个 DNS 查询 | Linux 中国

0
分享至

导读:事实证明,编写一段 120 行精巧的 Ruby 语言代码组成的程序就可以做到,这并不是很困难。

本文字数:11877,阅读时长大约: 15分钟

大家好!前段时间我写了一篇关于“jvns.ca”的帖子。

那篇帖子里我没写有关“如何生成以及解析 DNS 查询请求”的内容,因为我觉得这很无聊,不过一些伙计指出他们不知道如何解析和生成 DNS 查询请求,并且对此很感兴趣。

我开始好奇了——解析 DNS 能 花多大功夫?事实证明,编写一段 120 行精巧的 Ruby 语言代码组成的程序就可以做到,这并不是很困难。

所以,在这里有一个如何生成 DNS 查询请求,以及如何解析 DNS 响应报文的速成教学!我们会用 Ruby 语言完成这项任务,主要是因为不久以后我将在一场 Ruby 语言大会上发表观点,而这篇博客帖的部分内容是为了那场演讲做准备的。

(我尽量让不懂 Ruby 的人也能读懂,我只使用了非常基础的 Ruby 语言代码。)

最后,我们就能制作一个非常简易的 Ruby 版本的dig工具,能够查找域名,就像这样:

  1. $ ruby dig.rb example.com

  2. example.com 20314 A 93.184.216.34

整个程序大概 120 行左右,所以 并不 算多。(如果你想略过讲解,单纯想去读代码的话,最终程序在这里:gist.github.com。)

我们不会去实现之前帖中所说的“一个 DNS 解析器是如何运作的?”,因为我们已经做过了。

那么我们开始吧!

如果你想从头开始弄明白 DNS 查询是如何格式化的,我将尝试解释如何自己弄明白其中的一些东西。大多数情况下的答案是“用 Wireshark 去解包”和“阅读 RFC 1035,即 DNS 的规范”。

生成 DNS 查询请求

步骤一:打开一个 UDP 套接字

我们需要实际发送我们的 DNS 查询,因此我们就需要打开一个 UDP 套接字。我们会将我们的 DNS 查询发送至8.8.8.8,即谷歌的服务器。

下面是用于建立与8.8.8.8的 UDP 连接,端口为 53(DNS 端口)的代码。

  1. require 'socket'

  2. sock = UDPSocket.new

  3. sock.bind('0.0.0.0', 12345)

  4. sock.connect('8.8.8.8', 53)

关于 UDP 的说明

关于 UDP,我不想说太多,但是我要说的是,计算机网络的基础单位是“数据包(packet)”(即一串字节),而在这个程序中,我们要做的是计算机网络中最简单的事情:发送 1 个数据包,并接收 1 个数据包作为响应。

所以 UDP 是一个传递数据包的最简单的方法。

它是发送 DNS 查询最常用的方法,不过你还可以用 TCP 或者 DNS-over-HTTPS。

步骤二:从 Wireshark 复制一个 DNS 查询

下一步:假设我们都不知道 DNS 是如何运作的,但我们还是想尽快发送一个能运行的 DNS 查询。获取 DNS 查询并确保 UDP 连接正常工作的最简单方法就是复制一个已经正常工作的 DNS 查询!

所以这就是我们接下来要做的,使用 Wireshark (一个绝赞的数据包分析工具)。

我的操作大致如下:

1. 打开 Wireshark,点击 “ 捕获(capture)” 按钮。

2. 在搜索栏输入udp.port == 53作为筛选条件,然后按下回车。

3. 在我的终端运行ping example.com(用来生成一个 DNS 查询)。

4. 点击 DNS 查询(显示 “Standard query A example.com”)。 (“A”:查询类型;“ example.com”:域名;“Standard query”:查询类型描述)

5. 右键点击位于左下角面板上的 “ 域名系统(查询)(Domain Name System (query))”。

6. 点击 “ 复制(Copy)” ——> “ 作为十六进制流(as a hex stream)”。

7. 现在b96201000001000000000000076578616d706c6503636f6d0000010001就放到了我的剪贴板上,之后会用在我的 Ruby 程序里。好欸!

步骤三:解析 16 进制数据流并发送 DNS 查询

现在我们能够发送我们的 DNS 查询到8.8.8.8了!就像这样,我们只需要再加 5 行代码:

  1. hex_string = "b96201000001000000000000076578616d706c6503636f6d0000010001"

  2. bytes = [hex_string].pack('H*')

  3. sock.send(bytes, 0)

  4. # get the reply

  5. reply, _ = sock.recvfrom(1024)

  6. puts reply.unpack('H*')

[hex_string].pack('H*')意思就是将我们的 16 位字符串转译成一个字节串。此时我们不知道这组数据到底是什么意思,但是很快我们就会知道了。

我们还可以借此机会运用tcpdump,确认程序是否正常进行以及发送有效数据。我是这么做的:

1. 在一个终端选项卡下执行sudo tcpdump -ni any port 53 and host 8.8.8.8命令

2. 在另一个不同的终端指标卡下,运行 gist.github.com(ruby dns-1.rb

以下是输出结果:

  1. $ sudo tcpdump -ni any port 53 and host 8.8.8.8

  2. 08:50:28.287440 IP 192.168.1.174.12345 > 8.8.8.8.53: 47458+ A? example.com. (29)

  3. 08:50:28.312043 IP 8.8.8.8.53 > 192.168.1.174.12345: 47458 1/0/0 A 93.184.216.34 (45)

非常棒 —— 我们可以看到 DNS 请求(”这个example.com的 IP 地址在哪里?“)以及响应(“在93.184.216.34”)。所以一切运行正常。现在只需要(你懂的)—— 搞清我们是如何生成并解析这组数据的。

步骤四:学一点点 DNS 查询的格式

现在我们有一个关于example.com的 DNS 查询,让我们了解它的含义。

下方是我们的查询(16 位进制格式):

  1. b96201000001000000000000076578616d706c6503636f6d0000010001

如果你在 Wireshark 上搜索,你就能看见这个查询它由两部分组成:

请求头b96201000001000000000000

语句本身076578616d706c6503636f6d0000010001

步骤五:制作请求头

我们这一步的目标就是制作字节串b96201000001000000000000(借助一个 Ruby 函数,而不是把它硬编码出来)。

(LCTT 译注:硬编码(hardcode) 指在软件实现上,将输出或输入的相关参数(例如:路径、输出的形式或格式)直接以常量的方式撰写在源代码中,而非在运行期间由外界指定的设置、资源、数据或格式做出适当回应。)

那么:请求头是 12 个字节。那些个 12 字节到底意味着什么呢?如果你在 Wireshark 里看看(亦或者阅读 datatracker.ietf.org),你就能理解:它是由 6 个 2 字节大小的数字串联在一起组成的。

这六个数字分别对应查询 ID、标志,以及数据包内的问题计数、回答资源记录数、权威名称服务器记录数、附加资源记录数。

我们还不需要在意这些都是些什么东西 —— 我们只需要把这六个数字输进去就行。

但所幸我们知道该输哪六位数,因为我们就是为了直观地生成字符串b96201000001000000000000

所以这里有一个制作请求头的函数(注意:这里没有return,因为在 Ruby 语言里,如果处在函数最后一行是不需要写return语句的):

  1. def make_question_header(query_id)

  2. # id, flags, num questions, num answers, num auth, num additional

  3. [query_id, 0x0100, 0x0001, 0x0000, 0x0000, 0x0000].pack('nnnnnn')

  4. end

上面内容非常的短,主要因为除了查询 ID ,其余所有内容都由我们硬编码写了出来。

什么是nnnnnn?

可能能想知道.pack('nnnnnn')中的nnnnnn是个什么意思。那是一个向.pack()函数解释如何将那个 6 个数字组成的数据转换成一个字节串的一个格式字符串。

.pack的文档在 ruby-doc.org,其中描述了n的含义其实是“将其表示为” 16 位无符号、网络(大端序)字节序’”。

(LCTT 译注:大端序(Big-endian):指将高位字节存储在低地址,低位字节存储在高地址的方式。)

16 个位等同于 2 字节,同时我们需要用网络字节序,因为这属于计算机网络范畴。我不会再去解释什么是字节序了(尽管我确实有 wizardzines.com)。

测试请求头代码

让我们快速检测一下我们的make_question_header函数运行情况。

  1. puts make_question_header(0xb962) == ["b96201000001000000000000"].pack("H*")

这里运行后输出true的话,我们就成功了。

好了我们接着继续。

步骤六:为域名进行编码

下一步我们需要生成 问题本身(“example.com的 IP 是什么?”)。这里有三个部分:

域名(比如说example.com

查询类型(比如说A代表 “IPv4Address”)

查询类(总是一样的,1代表INternet)

最麻烦的就是域名,让我们写个函数对付这个。

example.com以 16 进制被编码进一个 DNS 查询中,如076578616d706c6503636f6d00。这有什么含义吗?

如果我们把这些字节以 ASCII 值翻译出来,结果会是这样:

  1. 076578616d706c6503636f6d00

  2. 7 e x a m p l e 3 c o m 0

因此,每个段(如example)的前面都会显示它的长度(7)。

下面是有关将example.com翻译成7 e x a m p l e 3 c o m 0的 Ruby 代码:

  1. def encode_domain_name(domain)

  2. domain

  3. .split(".")

  4. .map { |x| x.length.chr + x }

  5. .join + "\0"

  6. end

除此之外,,要完成问题部分的生成,我们只需要在域名结尾追加上(查询)的类型和类。

步骤七:编写 make_dns_query

下面是制作一个 DNS 查询的最终函数:

  1. def make_dns_query(domain, type)

  2. query_id = rand(65535)

  3. header = make_question_header(query_id)

  4. question = encode_domain_name(domain) + [type, 1].pack('nn')

  5. header + question

  6. end

这是目前我们写的所有代码 gist.github.com —— 目前仅 29 行。

接下来是解析的阶段

现在我尝试去解析一个 DNS 查询,我们到了硬核的部分:解析。同样的,我们会将其分成不同部分:

◈ 解析一个 DNS 的请求头

◈ 解析一个 DNS 的名称

◈ 解析一个 DNS 的记录

这几个部分中最难的(可能跟你想的不一样)就是:“解析一个 DNS 的名称”。

步骤八:解析 DNS 的请求头

让我们先从最简单的部分开始:DNS 的请求头。我们之前已经讲过关于它那六个数字是如何串联在一起的了。

那么我们现在要做的就是:

◈ 读其首部 12 个字节

◈ 将其转换成一个由 6 个数字组成的数组

◈ 为方便起见,将这些数字放入一个类中

以下是具体进行工作的 Ruby 代码:

  1. class DNSHeader

  2. attr_reader :id, :flags, :num_questions, :num_answers, :num_auth, :num_additional

  3. def initialize(buf)

  4. hdr = buf.read(12)

  5. @id, @flags, @num_questions, @num_answers, @num_auth, @num_additional = hdr.unpack('nnnnnn')

  6. end

  7. end

注:attr_reader是 Ruby 的一种说法,意思是“使这些实例变量可以作为方法使用”。所以我们可以调用header.flags来查看@flags变量。

我们也可以借助DNSheader(buf)调用这个,也不差。

让我们往最难的那一步挪挪:解析一个域名。

步骤九:解析一个域名

首先,让我们写其中的一部分:

  1. def read_domain_name_wrong(buf)

  2. domain = []

  3. loop do

  4. len = buf.read(1).unpack('C')[0]

  5. break if len == 0

  6. domain << buf.read(len)

  7. end

  8. domain.join('.')

  9. end

这里会反复读取一个字节的数据,然后将该长度读入字符串,直到读取的长度为 0。

这里运行正常的话,我们在我们的 DNS 响应头第一次看见了域名(example.com)。

关于域名方面的麻烦:压缩!

但当example.com第二次出现的时候,我们遇到了麻烦 —— 在 Wireshark 中,它报告上显示输出的域的值为含糊不清的 2 个字节的c00c

这种情况就是所谓的 DNS 域名压缩,如果我们想解析任何 DNS 响应我们就要先把这个实现完。

幸运的是,这没那么难。这里c00c的含义就是:

◈ 前两个比特(0b11.....)意思是“前面有 DNS 域名压缩!”

◈ 而余下的 14 比特是一个整数。这种情况下这个整数是120x0c),意思是“返回至数据包中的第 12 个字节处,使用在那里找的域名”

如果你想阅读更多有关 DNS 域名压缩之类的内容。我找到了相关更容易让你理解这方面内容的文章: datatracker.ietf.org。

步骤十:实现 DNS 域名压缩

因此,我们需要一个更复杂的read_domain_name函数。

如下所示:

  1. domain = []

  2. loop do

  3. len = buf.read(1).unpack('C')[0]

  4. break if len == 0

  5. if len & 0b11000000 == 0b11000000

  6. # weird case: DNS compression!

  7. second_byte = buf.read(1).unpack('C')[0]

  8. offset = ((len & 0x3f) << 8) + second_byte

  9. old_pos = buf.pos

  10. buf.pos = offset

  11. domain << read_domain_name(buf)

  12. buf.pos = old_pos

  13. break

  14. else

  15. # normal case

  16. domain << buf.read(len)

  17. end

  18. end

  19. domain.join('.')

这里具体是:

◈ 如果前两个位为0b11,那么我们就需要做 DNS 域名压缩。那么:

◈ 读取第二个字节并用一点儿运算将其转化为偏移量。

◈ 在缓冲区保存当前位置。

◈ 在我们计算偏移量的位置上读取域名

◈ 在缓冲区存储我们的位置。

可能看起来很乱,但是这是解析 DNS 响应的部分中最难的一处了,我们快搞定了!

一个关于 DNS 压缩的漏洞

有些人可能会说,有恶意行为者可以借助这个代码,通过一个带 DNS 压缩条目的 DNS 响应指向这个响应本身,这样read_domain_name就会陷入无限循环。我才不会改进它(这个代码已经够复杂了好吗!)但一个真正的 DNS 解析器确实会更巧妙地处理它。比如,这里有个 link.zhihu.com。

如果这是一个真正的 DNS 解析器,可能还有其他一些边缘情况会造成问题。

步骤十一:解析一个 DNS 查询

你可能在想:“为什么我们需要解析一个 DNS 查询?这是一个响应啊!”

但每一个 DNS 响应包含它自己的原始查询,所以我们有必要去解析它。

这是解析 DNS 查询的代码:

  1. class DNSQuery

  2. attr_reader :domain, :type, :cls

  3. def initialize(buf)

  4. @domain = read_domain_name(buf)

  5. @type, @cls = buf.read(4).unpack('nn')

  6. end

  7. end

内容不是太多:类型和类各占 2 个字节。

步骤十二:解析一个 DNS 记录

最让人兴奋的部分 —— DNS 记录是我们的查询数据存放的地方!即这个 “rdata 区域”(“记录数据字段”)就是我们会在 DNS 查询对应的响应中获得的 IP 地址所驻留的地方。

代码如下:

  1. class DNSRecord

  2. attr_reader :name, :type, :class, :ttl, :rdlength, :rdata

  3. def initialize(buf)

  4. @name = read_domain_name(buf)

  5. @type, @class, @ttl, @rdlength = buf.read(10).unpack('nnNn')

  6. @rdata = buf.read(@rdlength)

  7. end

我们还需要让这个rdata区域更加可读。记录数据字段的实际用途取决于记录类型 —— 比如一个“A” 记录就是一个四个字节的 IP 地址,而一个 “CNAME” 记录则是一个域名。

所以下面的代码可以让请求数据更可读:

  1. def read_rdata(buf, length)

  2. @type_name = TYPES[@type] || @type

  3. if @type_name == "CNAME" or @type_name == "NS"

  4. read_domain_name(buf)

  5. elsif @type_name == "A"

  6. buf.read(length).unpack('C*').join('.')

  7. else

  8. buf.read(length)

  9. end

  10. end

这个函数使用了TYPES这个哈希表将一个记录类型映射为一个更可读的名称:

  1. TYPES = {

  2. 1 => "A",

  3. 2 => "NS",

  4. 5 => "CNAME",

  5. # there are a lot more but we don't need them for this example

  6. }

read.rdata中最有趣的一部分可能就是这一行buf.read(length).unpack('C*').join('.')—— 像是在说:“嘿!一个 IP 地址有 4 个字节,就将它转换成一组四个数字组成的数组,然后数字互相之间用 ‘.’ 联个谊吧。”

步骤十三:解析 DNS 响应的收尾工作

现在我们正式准备好解析 DNS 响应了!

工作代码如下所示:

  1. class DNSResponse

  2. attr_reader :header, :queries, :answers, :authorities, :additionals

  3. def initialize(bytes)

  4. buf = StringIO.new(bytes)

  5. @header = DNSHeader.new(buf)

  6. @queries = (1..@header.num_questions).map { DNSQuery.new(buf) }

  7. @answers = (1..@header.num_answers).map { DNSRecord.new(buf) }

  8. @authorities = (1..@header.num_auth).map { DNSRecord.new(buf) }

  9. @additionals = (1..@header.num_additional).map { DNSRecord.new(buf) }

  10. end

  11. end

这里大部分内容就是在调用之前我们写过的其他函数来协助解析 DNS 响应。

如果@header.num_answers的值为 2,代码会使用了(1..@header.num_answers).map这个巧妙的结构创建一个包含两个 DNS 记录的数组。(这可能有点像 Ruby 魔法,但我就是觉得有趣,但愿不会影响可读性。)

我们可以把这段代码整合进我们的主函数中,就像这样:

  1. sock.send(make_dns_query("example.com", 1), 0) # 1 is "A", for IP address

  2. reply, _ = sock.recvfrom(1024)

  3. response = DNSResponse.new(reply) # parse the response!!!

  4. puts response.answers[0]

尽管输出结果看起来有点辣眼睛(类似于#),所以我们需要编写一些好看的输出代码,提升它的可读性。

步骤十四:对于我们输出的 DNS 记录进行美化

我们需要向 DNS 记录增加一个.to_s字段,从而让它有一个更良好的字符串展示方式。而者只是做为一行方法的代码在DNSRecord中存在。

  1. def to_s

  2. "#{@name}\t\t#{@ttl}\t#{@type_name}\t#{@parsed_rdata}"

  3. end

你可能也注意到了我忽略了 DNS 记录中的class区域。那是因为它总是相同的(IN 表示 “internet”),所以我觉得它是个多余的。虽然很多 DNS 工具(像真正的dig)会输出class

大功告成!

这是我们最终的主函数:

  1. def main

  2. # connect to google dns

  3. sock = UDPSocket.new

  4. sock.bind('0.0.0.0', 12345)

  5. sock.connect('8.8.8.8', 53)

  6. # send query

  7. domain = ARGV[0]

  8. sock.send(make_dns_query(domain, 1), 0)

  9. # receive & parse response

  10. reply, _ = sock.recvfrom(1024)

  11. response = DNSResponse.new(reply)

  12. response.answers.each do |record|

  13. puts record

  14. end

我不觉得我们还能再补充什么 —— 我们建立连接、发送一个查询、输出每一个回答,然后退出。完事儿!

  1. $ ruby dig.rb example.com

  2. example.com 18608 A 93.184.216.34

你可以在这里查看最终程序:gist.github.com。可以根据你的喜好给它增加更多特性,就比如说:

◈ 为其他查询类型添加美化输出。

◈ 输出 DNS 响应时增加“授权”和“可追加”的选项

◈ 重试查询

◈ 确保我们看到的 DNS 响应匹配我们的查询(ID 信息必须是对的上的!)

另外如果我在这篇文章中出现了什么错误,就 twitter.com。(我写的比较赶所以可能还是会有些错误)

(题图:MJ/449d049d-6bdd-448b-a61d-17138f8551bc)

via:

作者: 选题: 译者: 校对:

本文由 原创编译, 荣誉推出

LCTT 译者 :Xiaoyan Zhang

翻译: 3.0 篇

贡献: 282 天

2023-01-10

2023-10-19

https://linux.cn/lctt/Drwhooooo

欢迎遵照 CC-BY-SA 协议规定转载,

如需转载,请在文章下留言 “ 转载:公众号名称”,

我们将为您添加白名单,授权“ 转载文章时可以修改”。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
实在想不明白!一个球队替补,拿着300万底薪,为什么都叫他巨头

实在想不明白!一个球队替补,拿着300万底薪,为什么都叫他巨头

球毛鬼胎
2024-06-15 15:10:53
“难度直线飙升,堪比高考”!上海中考作文题公布,进来挑战→

“难度直线飙升,堪比高考”!上海中考作文题公布,进来挑战→

上观新闻
2024-06-15 18:19:51
秦岚带火了一种美拉德新穿法:夹克+短裙+乐福鞋,时髦洋气又减龄

秦岚带火了一种美拉德新穿法:夹克+短裙+乐福鞋,时髦洋气又减龄

蓓小西
2024-06-16 09:42:23
52岁大姐与57岁大哥,搭伙2年,大姐:他什么都好,就一点受不了

52岁大姐与57岁大哥,搭伙2年,大姐:他什么都好,就一点受不了

夫妻谈资局
2024-06-13 19:50:35
申花17岁前锋刘诚宇连续3场梅开二度,9球登顶U21联赛射手榜

申花17岁前锋刘诚宇连续3场梅开二度,9球登顶U21联赛射手榜

懂球帝
2024-06-15 21:34:10
10.61万亿经营贷,要爆了!

10.61万亿经营贷,要爆了!

说故事的阿袭
2024-06-14 17:35:56
又上当了!老美急哭:我只是想吓吓你们而已,不要当真啊

又上当了!老美急哭:我只是想吓吓你们而已,不要当真啊

芯怡飞
2024-06-10 10:49:06
浙大数学教授向姜萍发出诚挚邀请,与招生办唱起了对台戏!

浙大数学教授向姜萍发出诚挚邀请,与招生办唱起了对台戏!

娱乐的宅急便
2024-06-15 19:15:30
还记得上海电视台的主持人和晶吗?

还记得上海电视台的主持人和晶吗?

综艺拼盘汇
2024-06-16 07:25:09
小米SU7被曝电机故障率高,是英飞凌的锅,还是特斯拉的旧伤?

小米SU7被曝电机故障率高,是英飞凌的锅,还是特斯拉的旧伤?

路咖汽车
2024-06-15 17:31:12
0比3爆冷无缘4强!日本队头号种子被打懵,还敢放言赢中国队吗

0比3爆冷无缘4强!日本队头号种子被打懵,还敢放言赢中国队吗

体坛知识分子
2024-06-16 06:05:03
湖北设4站,这条高铁要来了

湖北设4站,这条高铁要来了

鲁中晨报
2024-06-16 10:35:05
江苏拿下全球三分之一订单,三市联手打造我国唯一集群

江苏拿下全球三分之一订单,三市联手打造我国唯一集群

江苏城市论坛
2024-06-15 21:18:55
十四届全国政协常委、文化文史和学习委员会主任吴英杰接受审查调查

十四届全国政协常委、文化文史和学习委员会主任吴英杰接受审查调查

界面新闻
2024-06-16 11:03:46
黄埔四期胡琏将军个人档案:撇开意识形态,不得不为其人格击节!

黄埔四期胡琏将军个人档案:撇开意识形态,不得不为其人格击节!

小刀99
2024-06-15 14:54:13
1960年,王光美和女儿在北戴河海滩

1960年,王光美和女儿在北戴河海滩

视点历史
2024-06-13 17:33:24
清华本硕,CPA+司考顶配的37岁金融男,被裁后竟然找不到工作,只求能还房贷…..

清华本硕,CPA+司考顶配的37岁金融男,被裁后竟然找不到工作,只求能还房贷…..

毯叔盘钱
2024-06-15 09:10:09
金融圈重磅!杨朝晖,任上被查!

金融圈重磅!杨朝晖,任上被查!

鲁中晨报
2024-06-15 12:11:05
G7发表联合公报要求俄向乌赔偿超4860亿美元损失,俄方暂未回应

G7发表联合公报要求俄向乌赔偿超4860亿美元损失,俄方暂未回应

环球网资讯
2024-06-15 14:24:10
144小时过境免签,回国后后遗症集体爆发,文明灯塔如何持续点亮

144小时过境免签,回国后后遗症集体爆发,文明灯塔如何持续点亮

囧囧智说
2024-06-15 14:20:33
2024-06-16 11:18:44
Linux
Linux
Linux 中国开源社区
8016文章数 73123关注度
往期回顾 全部

科技要闻

iPhone 16会杀死大模型APP吗?

头条要闻

40余套房屋涉嫌"一房多卖" 有购房者内心积郁因病去世

头条要闻

40余套房屋涉嫌"一房多卖" 有购房者内心积郁因病去世

体育要闻

没人永远年轻 但青春如此无敌还是离谱了些

娱乐要闻

江宏杰秀儿女刺青,不怕刺激福原爱?

财经要闻

打断妻子多根肋骨 上市公司创始人被公诉

汽车要闻

东风奕派eπ008售21.66万元 冰箱彩电都配齐

态度原创

本地
数码
时尚
教育
公开课

本地新闻

粽情一夏|海河龙舟赛,竟然成了外国人的大party!

数码要闻

小米 Redmi Buds 6 青春版耳机通过多项认证,搭载恒玄蓝牙方案

中年女性还是穿连衣裙最有气质!裙摆过膝、腰部收紧,巨显瘦

教育要闻

计算机专业,会是下一个土木吗?

公开课

近视只是视力差?小心并发症

无障碍浏览 进入关怀版