DNS 与 URL 转发

DNS 与世界

从硬件的角度来看,控制海底光缆等基础互联网设施,就控制了全世界;从软件的角度来看,控制了 DNS,也就控制了互联网。
互联网虽然叫国际互联网,但也分国家,全部控制,谈何容易?硬件层面的安全问题,会成为国家层面的安全问题,这是所有人可以看见的,所以这也无限拔升了控制的难度。
从软件的层面出发,控制 DNS 的可能性反倒会更高。一个 DNS client 上有没有后门留着,是很难知道的,特别是习以为常之后;万一有后门的话,且这个后门被激活,差不多就是毁天灭地的力量了。我们来看看市面上一般有哪些 DNS client,有微软 Windows 自带的、苹果 MacOS 自带的、苹果 iOS 自带的、谷歌安卓自带的、Linux 自带的,除了 Linux 之外 (开源的很难留后门,无强迫、没动机、有监督),其它要一次性控制,想想还是难度系数超高的,几乎不可能。但是,跟 控制所有互联网设备 相比,这个几乎不可能的难度系数就不值一提了。

控制会带来破坏性,但另外一方面,也会伴随着创造性。如果有新的协议,在整个 DNS 生态中获得通用性承认,就能在潜移默化之中推动整个互联网的进化,但这也非常困难,之前确实有这方面的尝试,比如 EDNS 这个接近理所当然的改进,但要做到 通用性,还需要很长很长很长的时间吧。

控制 DNS,站在整个互联网的角度来看,痴人说梦;但我们换一个角度,网络 是分层的,小的分层,可以是一个家庭内部的局域网,或者一个学校内的局域网,这个难度系数就直线降低,别的不说,如果就控制我们当前电脑上的 DNS,那是信手拈来的事情呀。
我们接手自己电脑上的 DNS 解析服务器,然后让所有的域名解析直接指向自己的某个 Web 服务器 (也可以是本地设备本身),那所有过来的 request,可以将其 proxy (代理),也可以 proxy 之后进行调整、优化。但这也仅限于 HTTP 的请求, HTTPS 的请求是加密的,需要更复杂些的 『授权』。
进一步发展,就是介入浏览器的行为,比如 UC 浏览器在低速互联网的时代中,就是通过介入对 request/response 的流程控制,才能提升页面的访问速度。此处就不具体展开说明了。

什么是 URL 转发

如果是通过 DNS 的手段预先拦截,再对一个 HTTP 的 Web请求 进行再处理,诈骗犯的骗术可以做到严丝合缝,所以有些网银在自己的宣传单中,会特别强调一个地址栏上的绿色锁 (表示当前是 HTTPS 访问)。但接管计算机设备的 DNS,还是蛮难的,所以市面上出现的是 钓鱼网站 这种诈骗方式。
但我们不会在此做什么特别的演示,本是很简单的事情,而且还是自己电脑设备上的操作,但外行的人看来,会是恐怖的事情。有心人要折腾你的话,那么,解释扩大化之后,联网之后的所有行为都是犯罪行为,请参考刑法第二百八十五条以及之后的几条内容。

我们要呈现的是 URL 转发,也是一些 国际域名服务商 会在 DNS 解析记录中提供的一种记录类型。它并不是真正的 DNS 记录类型,是一种为了用户体验而增加的逻辑。
之前已经大概介绍过 DNS 解析记录,我们应该很清楚 domain.comwww.domain.com 分别是两个不同的东西,一个是 根域名,一个是二级域名,但对于绝大多数的网民而言,两者应该是一样的。URL 转发 就是解决类似问题的, 比如当你访问 domain.com 的时候,会自动跳转www.domain.com,如果 domain.com 上设置了 URL 转发 的话。

先构建一个 Web 服务器

我们先用 Flask 完成一个 Web 服务器,它的用途很简单,就是处理 URL 的跳转,跳转的规则也很简单:比如访问 hello-domain.com 的时候,会自动跳转到 domain.com 上

from flask import Flask, redirect, request, abort
app = Flask(__name__)

@app.route('/')
@app.route('/<path:path>')
def auto_redirect(path=''):
    domain = request.host
    if domain.startswith('hello-'):
        original_domain = domain.replace('hello-', '', 1)
        new_url = 'http://%s/%s' % (original_domain, path.lstrip())
        return redirect(new_url)
    else:
        abort(404, '%s is not supported' % domain)

app.run(port=80)

调整 DNS 解析服务器的规则

也很简单,就是在 simple_dns_server.py 里,把 ip = get_ip_from_domain(domain) 这行改为:

if domain.startswith('hello-'):
    ip = '127.0.0.1'
else:
    ip = get_ip_from_domain(domain)

注意: 如果源代码中还有 time.sleep 的逻辑,请务必先移除,或者注释掉。

跑起来

方便起见,DNS 服务器对应的 .py 文件为 simple_dns_server.py,Web 服务器对应的 .py 文件为 simple_web_server.py

不使用 pipenv 的情况

新开两个命令行窗口,进入本篇对应的两个 .py 文件的所在目录,然后分别执行:

sudo python simple_dns_server.py

以及

sudo python simple_web_server.py

不论 DNS 还是 Web Server,都需要 sudo 权限,因为 Web Server 的 80 端口也是默认被 MacOS 限制的。

使用 pipenv 的情况

先打开两个命令行窗口,进入 pipenv 对应的项目根目录,然后执行 pipenv shell,进入命令行对应的虚拟环境。
然后,进入本篇对应的两个 .py 文件的所在目录,然后分别执行:

sudo python simple_dns_server.py

以及

sudo python simple_web_server.py

设置本地 DNS

具体参考《DNS 基础 · DNS 是怎么工作的》中的 『当前电脑的 DNS 服务器』这部分内容,我们将本地的 DNS 设置为 127.0.0.1,并且,不能再有其它的记录。

测试成果

现在我们要开始测试了。但在此之前,仍然有两点需要确认下:

  1. 不要处于完全的国际互联网的环境中 (比如 VPN),不然自己本地的 DNS 服务器地址的配置可能不会生效。
  2. 如果要访问 hello-domain.com,那么要确保之前浏览器没有访问过,不然浏览器的 DNS 缓存就会起作用,当前 127.0.0.1 则未必会去请求。

好了,现在打开浏览器,访问 hello-google.comhello-baidu.comhello-任何你想要访问的域名,看看效果是否已经达到了?这就是基于 DNS 的 URL 转发了,在互联网的环境中,只要别人将域名的 DNS 指向你的解析服务器,就能实现同样的效果。

扩充

除了 redirect 之外,你还可以对一个 HTTP 的 Web 请求直接 proxy,所谓的 proxy 就是 client 的 request 过来时,分两步走: 1, 从真正的外部获得内容;2,把第 1 步中获得的内容返回给 client。

如果跟 DNS 服务器继续合在一起,那么你需要注意如何获得 真正的外部 IP。比如说对 domain.com 进行了 proxy,那么 client 的 DNS 请求过来,你返回给 client 的是 127.0.0.1,那在自己的服务端,则不能去向 127.0.0.1 发送 Web 请求了,否则就成了无源之水。需要找到这个域名外部对应的真正 IP,这应该不难,在 《写一个 DNS 服务器》中就有现成的代码。
而我们对外发送一个 Web 请求时,表面上是访问某个域名下的 URL,实际上是请求到域名对应的 IP,而域名本身只是作为 request 的 headers (名为 Host) 传输过去。再回过头看看最开始的 Socket 编程,一个 Web 请求,就是连接 socket 的行为。

所有的技术瓶颈,都已经松动了,想怎么扩充,现在都是自由的了。