中国封锁全球网络审查项目OONI,影响测量数据提交和访问

近日,全球网络审查分析项目OONI发布公告

鉴于我们的工作和工具主要围绕测量和揭示中国和世界各地的互联网审查,而中国拥有全球最先进和普遍的互联网审查水平,这并不令人意外。然而,我们不确定为什么中国决定在此特定时刻开始封锁我们(而不是几年前),因为自2014年以来,我们已经从中国收集到了OONI的测量数据。OONI在中国的封锁时间(似乎始于2023年7月7日)使我们认为这可能与我们最近在2023年6月底关于F-Droid在中国的封锁有关。但是我们还在2019年报告了中国封锁维基百科的情况(以及其他关于中国审查的报告),当时中国并没有封锁我们的服务。

虽然我们过去曾注意到其他国家尝试封锁我们的服务(这就是为什么我们在OONI Probe中添加了后端代理支持,以规避对我们服务的任何意外或故意封锁),但这是我们首次看到由此导致的OONI测量覆盖范围大幅下降。

在这份报告中,我们分享了关于中国封锁OONI服务的OONI数据。

封锁OONI网站

从2023年7月7日开始,中国的几个网络似乎封锁了我们的网站(ooni.org)的访问。以下图表汇总了过去一个月内在中国网络中对ooni.org进行测试的OONI测量覆盖范围。

从上图可以看出,所有测量在2023年7月7日失败,此后收集的大多数测量要么失败,要么出现异常。虽然测量通常被注释为“失败”,当OONI Probe实验无法按预期执行时(例如由于错误),这些测量也可能是审查的症状。同时,当测量出现互联网审查的迹象时,它们被注释为“异常”(尽管可能会出现误报)。

值得注意的是,在此之前,从中国对ooni.org的测试收集的OONI测量显示ooni.org在中国的测试网络上是可访问的。只有2023年7月4日的一次测量出现异常(因为DNS查询导致超时错误),但这个单独的测量并不能提供关于审查的强烈信号(因为DNS解析返回了正确的IP地址)。

如果我们扩展在中国对ooni.org的测量覆盖范围的日期范围(从今年年初开始),我们可以更清楚地看到以前的大多数测量都是成功的(偶尔会有一些异常),而从2023年7月7日开始,失败和异常测量的比例增加了(如下图所示)。

image1_hu8959f9e31213f67bfc81716700490062_692998_1000x800_fit_box_3.png

 

上图不仅表明中国在2023年7月7日开始封锁对ooni.org的访问,还表明他们可能还开始封锁我们的后端服务,阻止OONI测量的提交。与之前的几个月相比,过去两周的测量覆盖范围下降,这一点暗示了这一点。我们将在报告的下一部分进一步探讨这个问题。

使用我们的Web连接性v0.5实验收集的数据说明了https://ooni.org的封锁情况。查询密钥显示使用系统和8.8.8.8:53/udp解析器进行DNS查找返回以下表中的值。

IP地址 AS号码 组织

2a03:2880:f12c:183:face:b00c:0:25de 32934 Facebook, Inc.

202.160.130.52 13414 Twitter Inc.

111.243.214.169 3462 Chunghwa Telecom Co., Ltd.

2001::caa0:80d2 N/A(bogon) N/A(bogon)

前三个IP地址显然对于ooni.org域名是错误的(托管在AS16509上),第四个IP地址是一个bogon IP地址(即不应该出现在公共IP网络上的IP地址)。向DNS查询注入属于大公司(如Facebook)的似乎随机的IP地址以审查DNS查询是中国防火墙的典型做法。

OONI数据显示测试助手无法连接到第二个和第三个IP地址。由于第四个IP地址是bogon,测试助手甚至不会尝试建立连接。测试助手可以连接到第一个IP地址,但显然TLS握手失败,因为给定的Facebook IP地址无法为ooni.org域名显示有效的X.509证书。

除了尝试使用TLS验证探测到的IP地址外,测试助手还为域名提供了有效的IP地址。在此测量实例中,测试助手将99.83.231.61和75.2.60.5返回给探测器。反过来,探测器使用这两个地址,但在TLS握手期间都失败了。

我们的DNS-over-UDP客户端在接收到第一个响应后会等待一段时间以获取重复的DNS响应。在此测量中,我们收到了发送到8.8.8.8:53/udp公共DNS解析器的每个DNS查询的两个重复响应。以下表格显示了我们收到的额外响应(其中第一个接收到的响应为1,第一个重复响应为2,依此类推)。

域名 类型 计数 地址 AS号码 组织

ooni.org AAAA 2 [ 2001::a27d:601 ] N/A(bogon) N/A(bogon)

ooni.org A 2 [ 108.160.165.173 ] 19679 Dropbox Inc

ooni.org AAAA 3 None N/A N/A

ooni.org A 3 [ 75.2.60.5, 99.83.231.61 ] 16509 Amazon

每个查询的第三个响应(第二个重复响应)包含正确的结果。这一事实强烈暗示存在中间盒子在合法响应到达探测器之前对查询进行响应,这是中国防火墙的众所周知的审查特性。

总结一下,我们得出结论,https://ooni.org/被DNS注入和TLS干扰的方式封锁。DNS封锁包括注入包含无效IP地址的响应。TLS封锁包括干扰TLS握手并重置TCP连接。

此外,OONI数据显示https://ooni.torproject.org/也被封锁。封锁此URL的方法与上述封锁https://ooni.org/的方法相同,我们观察到DNS注入和连接重置对TLS握手的干扰。

封锁OONI Probe

中国似乎还试图阻止OONI Probe用户提交测量数据。首先,这一点可以从过去两周中国OONI测量覆盖范围的大幅持续下降中推断出来。

image1_hu8959f9e31213f67bfc81716700490062_692998_1000x800_fit_box_3.png

 

测量覆盖范围的下降表明,中国的大多数OONI Probe用户可能无法再提交测量数据进行发布。这一点尤其表明,以前每天从中国收集到的测量数据量更大,并且过去两周内测量覆盖范围的下降一直持续存在。我们还观察到从2023年7月7日开始的Web连接性测量失败数量的总体增加,这与ooni.org封锁开始的日期相关。

要封锁OONI Probe,中国的ISP必须封锁我们的后端服务,阻止OONI Probe用户提交测量数据。因此,我们分析了通过使用Snowflake作为代理收集的与我们的API和测试助手相关的OONI数据。

以下表格总结了我们的发现。

测量 目标域名 bogons 错误地址 TCP/IP封锁 TLS封锁

数据 api.ooni.io ✔️ ✔️ ✔️

数据 0.th.ooni.org ✔️ ✔️ ✔️

数据 1.th.ooni.org ✔️ ✔️ ✔️

数据 2.th.ooni.org ✔️ ✔️ ✔️

数据 3.th.ooni.org ✔️ ✔️ ✔️

数据 dkyhjv0wpi2dk.cloudfront.net ✔️

bogons、错误地址和TLS封锁的审查条件与上述封锁https://ooni.org/的情况完全相同。实际上,我们可以将封锁https://ooni.org/的特征描述为bogons、错误地址和TLS封锁。TCP/IP封锁的审查条件表示我们无法建立TLS连接。成功表示我们可以成功与服务器通信。

api.ooni.io、0.th.ooni.org、1.th.ooni.org和2.th.ooni.org域名的封锁方式与ooni.org的封锁方式相同。

3.th.ooni.org域名与ooni.org不同之处在于DNS响应不包含bogons,而只包含与3.th.ooni.org域名无关的IP地址。此外,测试助手的IP地址也被TCP/IP封锁。我们不清楚为什么他们选择通过IP封锁此测试助手,而没有对其他测试助手采取同样的措施。

通过手动测试,我们确认了SNI 3.th.ooni.org也被过滤,并且过滤规则似乎适用于ooni.org或ooni.io的任何子域名。

最后,我们的cloudfront端点dkyhjv0wpi2dk.cloudfront.net只在TLS握手期间被封锁。本地解析器返回的所有IP地址都是合法的。

规避

为了使全球范围内的OONI Probe用户能够规避对我们服务的意外或故意封锁,OONI Probe移动应用程序包括后端代理设置。通过这些设置,您可以启用Psiphon或使用自定义代理提交OONI测量数据。

中国的OONI Probe用户可以在Android上安装Orbot并配置其使用Snowflake。然后,他们可以编辑OONI Probe移动应用程序的后端代理设置,将其设置为使用自定义代理,并将其指向127.0.0.1:9050。

具体而言,可以通过以下步骤完成:

  1. 从Play商店安装Orbot或从GitHub下载APK。
  2. 打开Orbot并启用“使用桥接”选项。
  3. 选择“通过其他使用Snowflake的Tor用户连接(方法1-快速)”,然后点击返回按钮。
  4. 通过点击大洋葱标志启动Orbot,并等待它启动。
  5. 在OONI Probe中进入设置-> OONI后端代理。
  6. 选择“自定义代理”,并将主机名设置为127.0.0.1,端口设置为9050。

如果您运行OONI Probe,现在应该能够使用Orbot和Snowflake提交测量数据进行发布。

重要提示:运行OONI Probe可能存在风险,尤其是在中国。现在中国的ISP正在封锁OONI服务,因此在中国运行OONI Probe可能会引起更多关注并带来更大的风险。

结论

自2014年以来,OONI测量数据已经从中国收集,记录了该国先进和普遍的互联网审查水平。最近封锁我们的后端服务意味着在中国运行OONI Probe现在更加困难(可能也更加危险),导致过去几周OONI测量覆盖范围的显著下降。

我们对此封锁特别担忧,因为来自中国的OONI数据已经成为该国互联网审查的大型开放数据集。从2014年至今,收集了来自193个网络的800多万个测量数据,OONI数据为过去9年中国的互联网审查情况提供了独特的见解。如果OONI封锁继续下去,将限制研究人员今后研究中国互联网审查的能力。

因此,这一封锁突出了我们需要改进规避能力的问题。虽然OONI Probe包括用于规避意外或故意封锁的后端代理设置,但还需要更多工作来提高OONI Probe的韧性,以确保在被审查的环境中仍然可以进行审查测量。

致谢

我们感谢过去9年在中国运行OONI Probe的所有人。

https://ooni.org/post/2023-china-blocks-ooni/

到底一台服务器上最多能创建多少个TCP连接

via https://plantegg.github.io/2020/11/30/%E4%B8%80%E5%8F%B0%E6%9C%BA%E5%99%A8%E4%B8%8A%E6%9C%80%E5%A4%9A%E8%83%BD%E5%88%9B%E5%BB%BA%E5%A4%9A%E5%B0%91%E4%B8%AATCP%E8%BF%9E%E6%8E%A5/

经常听到有同学说一台机器最多能创建65535个TCP连接,这其实是错误的理解,为什么会有这个错误的理解呢?

port range

我们都知道linux下本地随机端口范围由参数控制,也就是listen、connect时候如果没有指定本地端口,那么就从下面的port range 中随机取一个可用的


# cat /​proc/​sys/​net/​ipv4/​ip_​local_​port_​range

2000 65535

port range的上限是65535,所以也经常看到这个误解:一台机器上最多能创建65535个TCP连接

到底一台机器上最多能创建多少个TCP连接

先说结论:在内存、文件句柄足够的话可以创建的连接是没有限制的(每个TCP连接至少要消耗一个文件句柄)。

那么/​proc/​sys/​net/​ipv4/​ip_​local_​port_​range指定的端口范围到底是什么意思呢?

核心规则:一个TCP连接只要保证四元组(src-ip src-​port dest-​ip dest-port)唯一就可以了,而不是要求src port唯一

后面所讲都遵循这个规则,所以在心里反复默念:四元组唯一 五个大字,就能分析出来到底能创建多少TCP连接了。

比如如下这个机器上的TCP连接实际状态:


# netstat –ant |grep 18089

tcp 0 0 192.168.1.79:18089 192.168.1.79:22 ESTABLISHED

tcp 0 0 192.168.1.79:18089 192.168.1.79:18080 ESTABLISHED

tcp 0 0 192.168.0.79:18089 192.168.0.79:22 TIME_​WAIT

tcp 0 0 192.168.1.79:22 192.168.1.79:18089 ESTABLISHED

tcp 0 0 192.168.1.79:18080 192.168.1.79:18089 ESTABLISHED

从前三行可以清楚地看到18089被用了三次,第一第二行src-ip、dest-ip也是重复的,但是dest port不一样,第三行的src-port还是18089,但是src-ip变了。他们的四元组均不相同。

所以一台机器能创建的TCP连接是没有限制的,而ip_local_port_range是指没有bind的时候OS随机分配端口的范围,但是分配到的端口要同时满足五元组唯一,这样 ip_​local_​port_​range 限制的是连同一个目标(dest-ip和dest-port一样)的port的数量(请忽略本地多网卡的情况,因为dest-ip为以后route只会选用一个本地ip)。

那么为什么大家有这样的误解呢?我总结了下,大概是以下两个原因让大家误解了:

  • 如果是listen服务,那么肯定端口不能重复使用,这样就跟我们的误解对应上了,一个服务器上最多能监听65535个端口。比如nginx监听了80端口,那么tomcat就没法再监听80端口了,这里的80端口只能监听一次。
  • 另外如果我们要连的server只有一个,比如:1.1.1.1:80 ,同时本机只有一个ip的话,那么这个时候即使直接调connect 也只能创建出65535个连接,因为四元组中的三个是固定的了。

我们在创建连接前,经常会先调bind,bind后可以调listen当做服务端监听,也可以直接调connect当做client来连服务端。

bind(ip,port=0) 的时候是让系统绑定到某个网卡和自动分配的端口,此时系统没有办法确定接下来这个socket是要去connect还是listen. 如果是listen的话,那么肯定是不能出现端口冲突的,如果是connect的话,只要满足4元组唯一即可。在这种情况下,系统只能尽可能满足更强的要求,就是先要求端口不能冲突,即使之后去connect的时候四元组是唯一的。

比如 Nginx HaProxy envoy这些软件在创建到upstream的连接时,都会用 bind(0) 的方式, 导致到不同目的的连接无法复用同一个src port,这样后端的最大连接数受限于local_port_range。

Linux 4.2后的内核增加了IP_BIND_ADDRESS_NO_PORT 这个socket option来解决这个问题,将src port的选择延后到connect的时候
IP_​BIND_​ADDRESS_​NO_​PORT (since Linux 4.2)
Inform the kernel to not reserve an ephemeral port when using bind(2) with a port number of 0. The port will later be automatically chosen at connect(2) time, in a way that allows sharing a source port as long as the 4-​tuple is unique.

但如果我只是个client端,只需要连接server建立连接,也就不需要bind,直接调connect就可以了,这个时候只要保证四元组唯一就行。

bind()的时候内核是还不知道四元组的,只知道src_ip、src_port,所以这个时候单网卡下src_port是没法重复的,但是connect()的时候已经知道了四元组的全部信息,所以只要保证四元组唯一就可以了,那么这里的src_port完全是可以重复使用的。

640-20220224103024676.png

是不是加上了 SO_REUSEADDR、SO_REUSEPORT 就能重用端口了呢?

TCP SO_​REUSEADDR

文档描述:

SO_​REUSEADDR Indicates that the rules used in validating addresses supplied in a bind(2) call should allow reuse of local addresses. For AF_​INET sockets this means that a socket may bind, except when there is an active listening socket bound to the address. When the listening socket is bound to INADDR_​ANY with a specific port then it is not possible to bind to this port for any local address. Argument is an integer boolean flag.

从这段文档中我们可以知道三个事:

  1. 使用这个参数后,bind操作是可以重复使用local address的,注意,这里说的是local address,即ip加端口组成的本地地址,也就是两个本地地址,如果有任意ip或端口部分不一样,它们本身就是可以共存的,不需要使用这个参数。
  2. 当local address被一个处于listen状态的socket使用时,加上该参数也不能重用这个地址。
  3. 当处于listen状态的socket监听的本地地址的ip部分是INADDR_ANY,即表示监听本地的所有ip,即使使用这个参数,也不能再bind包含这个端口的任意本地地址,这个和 2 中描述的其实是一样的。

==SO_​REUSEADDR 可以用本地相同的(sip, sport) 去连connect 远程的不同的(dip、dport)//而 SO_​REUSEPORT主要是解决Server端的port重用==

SO_​REUSEADDR 还可以重用TIME_​WAIT状态的port, 在程序崩溃后之前的TCP连接会进入到TIME_WAIT状态,需要一段时间才能释放,如果立即重启就会抛出Address Already in use的错误导致启动失败。这时候可以通过在调用bind函数之前设置SO_REUSEADDR来解决。

What exactly does SO_​REUSEADDR do?
This socket option tells the kernel that even if this port is busy (in the TIME_​WAIT state), go ahead and reuse it anyway. If it is busy, but with another state, you will still get an address already in use error. It is useful if your server has been shut down, and then restarted right away while sockets are still active on its port. You should be aware that if any unexpected data comes in, it may confuse your server, but while this is possible, it is not likely.
It has been pointed out that “A socket is a 5 tuple (proto, local addr, local port, remote addr, remote port). SO_REUSEADDR just says that you can reuse local addresses. The 5 tuple still must be unique!” This is true, and this is why it is very unlikely that unexpected data will ever be seen by your server. The danger is that such a 5 tuple is still floating around on the net, and while it is bouncing around, a new connection from the same client, on the same system, happens to get the same remote port.

By setting SO_REUSEADDR user informs the kernel of an intention to share the bound port with anyone else, but only if it doesn’t cause a conflict on the protocol layer. There are at least three situations when this flag is useful:

  1. Normally after binding to a port and stopping a server it’s neccesary to wait for a socket to time out before another server can bind to the same port. With SO_REUSEADDR set it’s possible to rebind immediately, even if the socket is in a TIME_WAIT state.
  2. When one server binds to INADDR_ANY, say 0.0.0.0:1234, it’s impossible to have another server binding to a specific address like 192.168.1.21:1234. With SO_REUSEADDR flag this behaviour is allowed.
  3. When using the bind before connect trick only a single connection can use a single outgoing source port. With this flag, it’s possible for many connections to reuse the same source port, given that they connect to different destination addresses.

TCP SO_​REUSEPORT

SO_REUSEPORT主要用来解决惊群、性能等问题。通过多个进程、线程来监听同一端口,进来的连接通过内核来hash分发做到负载均衡,避免惊群。

SO_​REUSEPORT is also useful for eliminating the try-​10-​times-​to-​bind hack in ftpd’s data connection setup routine. Without SO_​REUSEPORT, only one ftpd thread can bind to TCP (lhost, lport, INADDR_​ANY, 0) in preparation for connecting back to the client. Under conditions of heavy load, there are more threads colliding here than the try-​10-​times hack can accomodate. With SO_​REUSEPORT, things work nicely and the hack becomes unnecessary.

SO_REUSEPORT使用场景:linux kernel 3.9 引入了最新的SO_REUSEPORT选项,使得多进程或者多线程创建多个绑定同一个ip:port的监听socket,提高服务器的接收链接的并发能力,程序的扩展性更好;此时需要设置SO_REUSEPORT(注意所有进程都要设置才生效)。

setsockopt(listenfd, SOL_​SOCKET, SO_REUSEPORT,(const void *)&reuse , sizeof(int));

目的:每一个进程有一个独立的监听socket,并且bind相同的ip:port,独立的listen()和accept();提高接收连接的能力。(例如nginx多进程同时监听同一个ip:port)

(a) on Linux SO_​REUSEPORT is meant to be used purely for load balancing multiple incoming UDP packets or incoming TCP connection requests across multiple sockets belonging to the same app. ie. it’s a work around for machines with a lot of cpus, handling heavy load, where a single listening socket becomes a bottleneck because of cross-​thread contention on the in-​kernel socket lock (and state).
(b) set IP_​BIND_​ADDRESS_​NO_​PORT socket option for tcp sockets before binding to a specific source ip
with port 0 if you’re going to use the socket for connect() rather then listen() this allows the kernel
to delay allocating the source port until connect() time at which point it is much cheaper

Ephemeral Port Range就是我们前面所说的Port Range(/proc/sys/net/ipv4/ip_local_port_range)

A TCP/​IPv4 connection consists of two endpoints, and each endpoint consists of an IP address and a port number. Therefore, when a client user connects to a server computer, an established connection can be thought of as the 4-​tuple of (server IP, server port, client IP, client port).
Usually three of the four are readily known – client machine uses its own IP address and when connecting to a remote service, the server machine’s IP address and service port number are required.
What is not immediately evident is that when a connection is established that the client side of the connection uses a port number. Unless a client program explicitly requests a specific port number, the port number used is an ephemeral port number.
Ephemeral ports are temporary ports assigned by a machine’s IP stack, and are assigned from a designated range of ports for this purpose. When the connection terminates, the ephemeral port is available for reuse, although most IP stacks won’t reuse that port number until the entire pool of ephemeral ports have been used.
So, if the client program reconnects, it will be assigned a different ephemeral port number for its side of the new connection.

linux 如何选择Ephemeral Port

有资料说是随机从Port Range选择port,有的说是顺序选择,那么实际验证一下。

如下测试代码:


#include <stdio.h> /​/​printf

#include <stdlib.h> /​/​atoi

#include <unistd.h> /​/​close

#include <arpa/inet.h> /​/​ntohs

#include <sys/socket.h> /​/​connect, socket

void sample() {

/​/​Create socket

int sockfd;

if (sockfd = socket(AF_INET, SOCK_​STREAM, 0), –1 == sockfd) {

perror(“socket”);

/​/​Connect to remote. This does NOT actually send a packet.

const struct sockaddr_​in raddr = {

.sin_​family = AF_​INET,

.sin_​port = htons(8080), /​/​arbitrary remote port

.sin_​addr = htonl(INADDR_ANY) /​/​arbitrary remote host

if (-1 == connect(sockfd, (const struct sockaddr *)&raddr, sizeof(raddr))) {

perror(“connect”);

/​/​Display selected ephemeral port

const struct sockaddr_​in laddr;

socklen_​t laddr_​len = sizeof(laddr);

if (-1 == getsockname(sockfd, (struct sockaddr *)&laddr, &laddr_​len)) {

perror(“getsockname”);

printf(“local port: %i\n”, ntohs(laddr.sin_port));

/​/​Close socket

close(sockfd);

int main() {

for (int i = 0; i < 5; i++) {

bind逻辑测试代码


#include <netinet/in.h>

#include <arpa/inet.h>

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <errno.h>

#include <string.h>

#include <sys/types.h>

#include <time.h>

void test_​bind(){

int listenfd = 0, connfd = 0;

struct sockaddr_​in serv_​addr;

char sendBuff[1025];

time_​t ticks;

socklen_​t len;

listenfd = socket(AF_INET, SOCK_​STREAM, 0);

memset(&serv_addr, ‘0’, sizeof(serv_addr));

memset(sendBuff, ‘0’, sizeof(sendBuff));

serv_addr.sin_family = AF_​INET;

serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

serv_addr.sin_port = htons(0);

bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

len = sizeof(serv_addr);

if (getsockname(listenfd, (struct sockaddr *)&serv_​addr, &len) == –1) {

perror(“getsockname”);

printf(“port number %d\n”, ntohs(serv_addr.sin_port)); //只是挑选到了port,在系统层面保留,tcp连接还没有,netstat是看不到的

int main(int argc, char *argv[])

for (int i = 0; i < 5; i++) {

test_​bind();

3.10.0–327.ali2017.alios7.x86_64

编译后,执行(3.10.0–327.ali2017.alios7.x86_64):


#date; ./​client && echo “+++++++” ; ./​client && sleep 0.1 ; echo “——-” && ./​client && sleep 10; date; ./​client && echo “+++++++” ; ./​client && sleep 0.1 && echo “******”; ./​client;

Fri Nov 27 10:52:52 CST 2020

local port: 17448

local port: 17449

local port: 17451

local port: 17452

local port: 17453

local port: 17455

local port: 17456

local port: 17457

local port: 17458

local port: 17460

local port: 17475

local port: 17476

local port: 17477

local port: 17478

local port: 17479

Fri Nov 27 10:53:02 CST 2020

local port: 17997

local port: 17998

local port: 17999

local port: 18000

local port: 18001

local port: 18002

local port: 18003

local port: 18004

local port: 18005

local port: 18006

local port: 18010

local port: 18011

local port: 18012

local port: 18013

local port: 18014

从测试看起来linux下端口选择跟时间有关系,起始端口肯定是顺序增加,起始端口应该是在Ephemeral Port范围内并且和时间戳绑定的某个值(也是递增的),即使没有使用任何端口,起始端口也会随时间增加而增加。

4.19.91–19.1.al7.x86_64

换个内核版本编译后,执行(4.19.91–19.1.al7.x86_64):


$date; ./​client && echo “+++++++” ; ./​client && sleep 0.1 ; echo “——-” && ./​client && sleep 10; date; ./​client && echo “+++++++” ; ./​client && sleep 0.1 && echo “******”; ./​client;

Fri Nov 27 14:10:47 CST 2020

local port: 7890

local port: 7892

local port: 7894

local port: 7896

local port: 7898

local port: 7900

local port: 7902

local port: 7904

local port: 7906

local port: 7908

local port: 7910

local port: 7912

local port: 7914

local port: 7916

local port: 7918

Fri Nov 27 14:10:57 CST 2020

local port: 7966

local port: 7968

local port: 7970

local port: 7972

local port: 7974

local port: 7976

local port: 7978

local port: 7980

local port: 7982

local port: 7984

local port: 7988

local port: 7990

local port: 7992

local port: 7994

local port: 7996

以上测试时的参数


$cat /​proc/​sys/​net/​ipv4/​ip_​local_​port_​range

1024 65535

将1024改成1025后,分配出来的都是奇数端口了:


$cat /​proc/​sys/​net/​ipv4/​ip_​local_​port_​range

local port: 1033

local port: 1025

local port: 1027

local port: 1029

local port: 1031

local port: 1033

local port: 1025

local port: 1027

local port: 1029

local port: 1031

local port: 1033

local port: 1025

local port: 1027

local port: 1029

local port: 1031

之所以都是偶数端口,是因为port_range 从偶数开始, 每次从++变到+2的原因,connect挑选随机端口时都是在起始端口的基础上+2,而bind挑选随机端口的起始端口是系统port_range起始端口+1(这样和connect错开),然后每次仍然尝试+2,这样connect和bind基本一个用偶数另外一个就用奇数,一旦不够了再尝试使用另外一组


$cat /​proc/​sys/​net/​ipv4/​ip_​local_​port_​range

$./​bind & —bind程序随机挑选5个端口

port number 1039

port number 1043

port number 1045

port number 1041

port number 1047 –用完所有奇数端口

$./​bind & –继续挑选偶数端口

port number 1044

port number 1042

port number 1046

port number 0 –实在没有了

port number 0

可见4.19内核下每次port是+2,在3.10内核版本中是+1. 并且都是递增的,同时即使port不使用,也会随着时间的变化这个起始port增大。

Port Range有点像雷达转盘数字,时间就像是雷达上的扫描指针,这个指针不停地旋转,如果这个时候刚好有应用要申请Port,那么就从指针正好指向的Port开始向后搜索可用port

tcp_​max_​tw_​buckets

tcp_​max_​tw_​buckets: 在 TIME_​WAIT 数量等于 tcp_​max_​tw_​buckets 时,新的连接断开不再进入TIME_WAIT阶段,而是直接断开,并打印warnning.

实际测试发现 在 TIME_​WAIT 数量等于 tcp_​max_​tw_​buckets 时 新的连接仍然可以不断地创建和断开,这个参数大小不会影响性能,只是影响TIME_WAIT 数量的展示(当然 TIME_​WAIT 太多导致local port不够除外), 这个值设置小一点会避免出现端口不够的情况

tcp_​max_​tw_​buckets — INTEGER
Maximal number of timewait sockets held by system simultaneously.If this number is exceeded time-​wait socket is immediately destroyed and warning is printed. This limit exists only to prevent simple DoS attacks, you must not lower the limit artificially, but rather increase it (probably, after increasing installed memory), if network conditions require more than default value.

SO_​LINGER选项用来设置延迟关闭的时间,等待套接字发送缓冲区中的数据发送完成。 没有设置该选项时,在调用close() 后,在发送完FIN后会立即进行一些清理工作并返回。 如果设置了SO_LINGER选项,并且等待时间为正值,则在清理之前会等待一段时间。

如果把延时设置为 0 时,Socket就丢弃数据,并向对方发送一个 RST 来终止连接,因为走的是 RST 包,所以就不会有 TIME_WAIT 了。

This option specifies how the close function operates for a connection-​oriented protocol (for TCP, but not for UDP). By default, close returns immediately, but ==if there is any data still remaining in the socket send buffer, the system will try to deliver the data to the peer==.

SO_​LINGER 有三种情况

  1. l_​onoff 为false(0), 那么 l_​linger 的值没有意义,socket主动调用close时会立即返回,操作系统会将残留在缓冲区中的数据发送到对端,并按照正常流程关闭(交换FIN-ACK),最后连接进入TIME_WAIT状态。这是默认情况
  2. l_​onoff 为true(非0), l_​linger 为0,主动调用close的一方也是立刻返回,但是这时TCP会丢弃发送缓冲中的数据,而且不是按照正常流程关闭连接(不发送FIN包),直接发送RST,连接不会进入 time_​wait 状态,对端会收到 java.net.SocketException: Connection reset异常
  3. l_​onoff 为true(非0), l_​linger 也为非 0,这表示 SO_LINGER选项生效,并且超时时间大于零,这时调用close的线程被阻塞,TCP会发送缓冲区中的残留数据,这时有两种可能的情况:
    • 数据发送完毕,收到对方的ACK,然后进行连接的正常关闭(交换FIN-ACK)
    • 超时,未发送完成的数据被丢弃,连接发送RST进行非正常关闭

struct linger {

int l_​onoff; /​* 0=off, nonzero=on */​

int l_​linger; /​* linger time, POSIX specifies units as seconds */​

NIO下设置 SO_​LINGER 的错误案例

在使用NIO时,最好不设置SO_LINGER。比如Tomcat服务端接收到请求创建新连接时,做了这样的设置:


SocketChannel.setOption(SocketOption.SO_LINGER, 1000)

SO_LINGER的单位为!在网络环境比较好的时候,例如客户端、服务器都部署在同一个机房,close虽然会被阻塞,但时间极短可以忽略。但当网络环境不那么好时,例如存在丢包、较长的网络延迟,buffer中的数据一直无法发送成功,那么问题就出现了:close会被阻塞较长的时间,从而直接或间接引起NIO的IO线程被阻塞,服务器会不响应,不能处理accept、read、write等任何IO事件。也就是应用频繁出现挂起现象。解决方法就是删掉这个设置,close时立即返回,由操作系统接手后面的工作。

这时会看到如下连接状态

image-20220721100246598.png

以及对应的堆栈

image-20220721100421130.png

查看其中一个IO线程等待的锁,发现锁是被HTTP线程持有。这个线程正在执行preClose0,就是在这里等待连接的关闭image-20220721100446521.png

每次HTTP线程在关闭连接被阻塞时,同时持有了SocketChannelImpl的对象锁,而IO线程在把这个连接移除出它的selector管理队列时,也要获得同一个SocketChannelImpl的对象锁。IO线程就这么一次次的被阻塞,悲剧的无以复加。有些NIO框架会让IO线程去做close,这时候就更加悲剧了。

总之这里的错误原因有两点:1)网络状态不好;2)错误理解了l_linger 的单位,是秒,不是毫秒。 在这两个原因的共同作用下导致了数据迟迟不能发送完毕,l_linger 超时又需要很久,所以服务会出现一直阻塞的状态。

为什么要有 time_​wait 状态

TIME-​WAIT — represents waiting for enough time to pass to be sure the remote TCP received the acknowledgment of its connection termination request.

image-20220721093116395.png

这个案例来自腾讯7层网关团队,网关用的Nginx,请求转发给后面的被代理机器(RS:real server),发现 sys CPU异常高,CPU都用在搜索可用端口.

640-8259033.png

640-20221112211814567.png

local port 不够的时候inet_​hash_​connect 中的spin_​lock 会消耗过高的 sys(特别注意4.6内核后 local port 分奇偶数,每次loop+2,所以更容易触发port不够的场景)

核心原因总结: 4.6后内核把本地端口分成奇偶数,奇数给connect, 偶数给listen,本来端口有6万,这样connect只剩下3万,当这3万用完后也不会报找不到本地可用端口的错误(这里报错可能更好),而是在奇数里找不到就找偶数里的,每次都这样。 没改以前,总共6万端口,用掉3万,不分奇偶的话那么每找两个端口就有一个能用,也就是50%的概率。但是改了新的实现方案后,每次先要找奇数的3万个,全部在用,然后到偶数里继续找到第30001个才是可用的,也就是找到的概率变成了3万分之一,一下子复杂度高了15000倍,不慢才怪 如果你对

我的看法,这个分奇偶数的实现就是坑爹货,在内核里胡乱搞,为了一个小场景搞崩大多数正常场景,真没必要,当然我这是事后诸葛亮,如果当时这种feature拿给我看我也会认为很不错,想不到这个坑点!

listen port search消耗CPU异常高

640-9840722.jpeg

在正常的情况下,服务器的listen port数量,大概就是几w个这样的量级。这种量级下,一个port对应一个socket,哈希桶大小为32是可以接受的。

然而在内核支持了reuseport并且被广泛使用后,情况就不一样了,在多进程架构里,listen port对应的socket数量,是会被几十倍的放大的。以应用层监听了5000个端口,reuseport 使用了50个cpu核心为例,5000*50/32约等于7812,意味着每次握手包到来时,光是查找listen socket,就需要遍历7800多次。随着机器硬件性能越来越强,应用层使用的cpu数量增多,这个问题还会继续加剧。

正因为上述原因,并且我们现网机器开启了reuseport,在端口数量较多的机器里,inet_lookup_listener的哈希桶大小太小,遍历过程消耗了cpu,导致出现了函数热点。

短连接的开销

用ab通过短连接走 lo 网卡压本机 nginx,CPU0是 ab 进程,CPU3/4 是 Nginx 服务,可以看到 si 非常高,QPS 2.2万

image-20220627154822263.png

再将 ab 改用长连接来压,可以看到si、sy都有下降,并且 si 下降到短连接的20%,QPS 还能提升到 5.2万

image-20220627154931495.png

主要是内存开销(如图,来源见水印),另外就是每个连接都会占用一个文件句柄,可以通过参数来设置:fs.nr_open、nofile(其实 nofile 还分 soft 和 hard) 和 fs.file-max

640-20220413134252639

从上图可以看到:

  • 没有收发数据的时候收发buffer不用提前分配,3K多点的内存是指一个连接的元信息数据空间,不包含传输数据的内存buffer

  • 客户端发送数据后,会根据数据大小分配send buffer(一般不超过wmem,默认kernel会根据系统内存压力来调整send buffer大小)

  • server端kernel收到数据后存放在rmem中,应用读走后就会释放对应的rmem

  • rmem和wmem都不会重用,用时分配用完释放

可见,内核在 socket 内存开销优化上采取了不少方法:

  • 内核会尽量及时回收发送缓存区、接收缓存区,但高版本做的更好
  • 发送接收缓存区最小并一定不是 rmem 内核参数里的最小值,实际大部分时间都是0
  • 其它状态下,例如对于TIME_WAIT还会回收非必要的 socket_​alloc 等对象

A进程选择某个端口,并设置了 reuseaddr opt(表示其它进程还能继续用这个端口),这时B进程选了这个端口,并且bind了,B进程用完后把这个bind的端口释放了,但是如果 A 进程一直不释放这个端口对应的连接,那么这个端口会一直在内核中记录被bind用掉了(能bind的端口 是65535个,四元组不重复的连接你理解可以无限多),这样的端口越来越多后,剩下可供 A 进程发起连接的本地随机端口就越来越少了(也就是本来A进程选择端口是按四元组的,但因为前面所说的原因,导致不按四元组了,只按端口本身这个一元组来排重),这时会造成新建连接的时候这个四元组高概率重复,一般这个时候对端大概率还在 time_wait 状态,会忽略掉握手 syn 包并回复 ack ,进而造成建连接卡顿的现象

结论

  • 在内存、文件句柄足够的话一台服务器上可以创建的TCP连接数量是没有限制的
  • SO_​REUSEADDR 主要用于快速重用 TIME_WAIT状态的TCP端口,避免服务重启就会抛出Address Already in use的错误
  • SO_REUSEPORT主要用来解决惊群、性能等问题
  • 全局范围可以用 net.ipv4.tcp_max_tw_buckets = 50000 来限制总 time_​wait 数量,但是会掩盖问题
  • local port的选择是递增搜索的,搜索起始port随时间增加也变大

参考资料

https://segmentfault.com/a/1190000002396411

linux中TCP的socket、bind、listen、connect和accept的实现

How Linux allows TCP introspection The inner workings of bind and listen on Linux.

https://idea.popcount.org/2014–04-03-bind-before-connect/

TCP连接中客户端的端口号是如何确定的?

对应4.19内核代码解析

How to stop running out of ephemeral ports and start to love long-​lived connections


谷歌提议对网络进行DRM - 破坏开放性

via https://here.news/post/64c1eb5f39cc7a79010a22c1

Picked image

谷歌试图巩固Chrome在网络标准上的垄断地位,阻止广告拦截,摧毁Firefox和较小的Chromium和WebKit分支,并提高其广告收入。

此(拟议中的)API允许网站从“认证者”那里请求“认证”,以验证客户端环境。谷歌可以通过将Chrome作为认证者来利用这一点,从而巧妙地提升Chrome在Firefox等其他浏览器上的优势。这是伪装成的供应商锁定!

“认证者”决定您的设备和/或浏览器是否足够“值得信赖”-这是由您试图访问的网站定义的。

它专门设计用于摧毁开放网络,拒绝您使用任何您想使用的浏览器,在任何操作系统上。

另一个担忧在于限制广告拦截器。通过Manifest v3,谷歌打算限制Chrome扩展中的广告拦截功能。结合WEI API,谷歌可能会在浏览体验、垄断和广告收入方面获得更多控制权。

这个API几乎没有什么好处,但可能最终会伤害到小型浏览器分支、修改过的Android/iOS手机、定制Linux发行版、用户隐私(通过向认证者提供更多数据点并限制隐私工具)、用户体验(通过破坏广告拦截)。

提案的官方GitHub链接:https://github.com/RupertBenWiser/Web-Environment-Integrity (大家去查看问题和PRs)

家人的 Apple ID 开了双重认证,仍然被钓鱼,求大佬解惑,也顺便给大家提个醒

via https://www.v2ex.com/t/959041

时间线

7 月 12 晚上发生的事情,

  • 23:33 ,丈母娘的手机突然被抹掉了资料,变成出厂设置的状态。
  • 23:35 ,她拿手机找我给她看看,我以为是苹果系统问题,开始给她重新设置。
  • 23:36 ,在设置的过程中,手机陆续收到了短信通知,我发现其中有银行、支付等字样。
  • 23:37 ,开始意识到事情不太对,赶紧联系银行和微信支付冻结。
  • 23:40 ,等到冻结完毕,已经产生了 20 多笔订单,共计 1.6w 。
  • 23:50 ,报警之后,到社区派出所立案。
  • 01:10 ,立案过程中,我在 Apple Store 的退款渠道提交了退款。

被盗经过

之前一直以为开了双重认证就高枕无忧,经过排查后,基本确定是被钓鱼了:

  • 丈母娘曾经在某 App 购买虚拟商品,App Store 绑定了微信免密支付。

  • 7 月 11 号下午,丈母娘在 Apple Store 上下载了一个叫 “菜谱大全” 的 App ,它的登录方式是 Apple ID 授权,这一步如果没有开启 iCloud+ 隐藏邮件地址的话,Apple ID 账号就会泄露,如图

  • 接着,会出来一个跟 App Store 长得非常像的密码输入框,大家如果经常安装 App ,人脸识别失败的时候,就会有这个密码输入框,不熟悉 App Store 登录流程的话,很容易中招,如图

  • 有了 Apple ID 的账号和密码,就可以登录了,这一步我跟丈母娘反复确认了,她没有见过双重认证的弹窗。

  • 登录之后,他会把自己的号码,加入双重认证的信任号码中,目的是为了后续的登录可以通过自己认证,如图

  • 到这一步,他已经掌握了受害者 Apple ID 的所有权限。

  • 接下来,盗号者并不会直接用 Apple ID 下单支付,而是会创建一个家庭共享,加入另一个账号,由这个账号购买 App 中的虚拟商品,如图

疑问

整个钓鱼过程,我有一点不太理解,在开启了双重认证的情况下,除非我丈母娘主动输入验证码,否则即使对方拿到了 Apple ID 的账号密码,应该也无法登录才对,这里请大佬帮忙解惑。

尝试退款

我在 Apple 400 客服尝试了多种方式,最终都失败了:

  • reportaproblem.apple.com 页面申请退款,申请后联系客服告知被拒绝。
  • 找负责 App Store 订单的客服,要求升级高级顾问,告知这是最终结果,升级也没有意义,被拒绝。
  • 找负责 Apple ID 的客服,曲线救国,要求查询 Apple ID 被盗的问题,被告知查不到记录。
  • 由负责 Apple ID 的高级顾问转到负责 App Store 订单的高级顾问,和该顾问扯皮了 2 小时,被拒绝。

目前还能尝试的方式:

  • 打 12315 反馈
  • 在工信部违法和不良信息举报中心投诉
  • 起诉苹果
第 1 条附言  ·  23 小时 29 分钟前
补充一下:
抹除设备是为了防止盗号者在支付的时候,受害者微信出现支付通知。
评论区有大佬提到,如果这个 APP 有截屏,可以截到双重认证的弹窗并上传,并不需要 Apple ID 所有者主动提供验证码。
第 2 条附言  ·  22 小时 57 分钟前
这个 App 的权限只有两个:Siri 与搜索,无线数据
第 3 条附言  ·  10 小时 58 分钟前
抓包看了下,app 会访问这个 app.yime888.com ,有没有大佬有兴趣爆破一下。
第 4 条附言  ·  8 小时 35 分钟前
89 楼的大佬,给了一个绕过双重认证的思路,感觉是比较靠谱的。
第 5 条附言  ·  6 小时 48 分钟前
感谢各位大佬,目前差不多搞清楚对方是如何绕过双重认证的:
对方在 App 内置了一个 Webview ,然后访问 appleid.apple.com/sign-in ,这一步系统会出现 Apple ID 的弹窗,如果人脸识别通过了或者输入了正确的密码,这个页面就登录了(可以理解为在内置的 safafi 打开了 Apple ID 的登录页面)。

接下来会出现密码弹窗,受害者输入密码之后,这个 Webview 可以注入一些 js 获取到 Cookie ,然后访问 appleid.apple.com/account/manage ,通过一些自动接收验证码的机制,配合 Cookie 和密码,就可以在受害者 Apple ID 的信任号码中加入他自己的号码,用来接收双重认证的短信

中国的网络行为在海底光缆生态系统中得到体现

via https://here.news/post/64b4fffa791292e41eb8d586

随着世界渴望数字化,实现经济一体化和超越边界,全球第二大经济体中国不能置身事外。但中国在网络空间的行动显示了中国打算以一种不同的方式进行数字化过程。中国在数字化和网络安全方面制定国际规范的意图可以从其对网络空间变化的看法和反应中得到体现。中国在网络空间中对“数字主权”的理解正在为全球数字治理设定平行标准,这些标准基于封闭和不互动的网络空间。

嵌入水下的海底光缆是运行网络空间的基础,通常似乎不受此类行动的影响。但中国在网络领域的三个具体特点在海底光缆生态系统中也得到延续。

中国知道,特别是在发展中国家和欠发达国家,成本效益优先于质量。习近平于2015年提出的数字丝绸之路战略旨在帮助参与国发展和提升数字基础设施。为了建立这个数字生态系统,中国公司如中兴、大华、华为等在西亚和北非国家备受青睐,因为它们提供低成本的服务,推动了数字丝绸之路的实现。同样,中国的海底光缆公司华为海洋网络有限公司(HMN Tech)也以更低的成本提供光缆制造、铺设、维护和修复服务。尽管成立于2008年,HMN Tech已经获得全球四分之一的光缆维护和修复合同,并成为最快的海底光缆铺设公司。尽管由于美国的秘密外交,中国在Sea-Me-We 6光缆的投标过程中失去了竞标,但其最终报价只有原始报价的三分之一。

此外,基于价格,中国满足了全球南方国家的需求,这些国家更有可能成为中国制定数字标准的一方。除了其周边地区之外,HMN Tech的几乎所有海底光缆项目都位于全球南方。

中国确保与全球数字网络的最小整合,以维护其数字主权。中国的“防火长城”是各种技术和政府政策的产物,禁止大量数据进入中国的数字空间。尽管中国渴望保留来自其他国家流入其领土的最大信息量,但它不希望将自己整合并将自己的信息提供给全球数字网络,以确保数据安全和数据隐私的幌子下。对从其数字空间流出的数据有非常严格的监管。它在海底光缆生态系统中也保持着同样的行为。世界上最大的经济体美国有大约85条海底光缆登陆其海岸,而全球第二大经济体中国只有大约19条这样的海底光缆。较少的海底光缆意味着更少的数据交换和较慢的数据传播。虽然中国的公司HMN Tech正在全球范围内开展约134个海底光缆项目,但中国在其领土上目前只计划在不久的将来铺设4条海底光缆,与国际海底光缆网络的整合较少。这是对数字主权的延伸。

中国的私营公司,特别是那些包含对维护中国稳定和安全至关重要的信息的公司,在后台受到政府的严格控制。一直有传言称电信制造公司华为与共产党有关联,导致一些国家担心通过华为的设备进行监视和间谍活动而退出华为的5G试验。这种强大的政府控制在海底光缆生态系统中也得到延续。当HMN Tech在SeaMeWe-6光缆的投标过程中失败时,中国两家电信巨头中国移动和中国电信退出了该项目,作为反对的标志。同样,政府还向HMN Tech提供了巨额补贴,以承担一项价值5亿美元的项目,该项目将连接亚洲、中东和欧洲,并成为SeaMeWe-6项目的竞争对手。

一些人认为,中国通过窃听海底光缆进行间谍活动的担忧是夸大其词的。但是,仔细审查这些从网络空间延伸到海底光缆的连续性,暗示了在海底光缆生态系统中可能出现类似情况的未来可能性。据称,华为的监视设备曾对其他国家的政府机构进行监视。有报告暗示全球范围内发生了类似的监视事件。

尽管故意切断海底光缆干扰全球数字数据流尚未成为常态,但全球数字化需求的增加和对海底光缆架构的重视已经在中美技术战争中开辟了一个新的战线。最近,中国据称破坏了马祖岛的海底光缆,对台湾外岛进行了约六周的信息封锁。如果通过这些海底光缆流动的信息可以停止,那么也可以提取。信息的停止和提取的致命组合可以在权力动态中产生巨大的转变,因为数据成为最尖锐的双刃剑之一。了解这些反映是重要的,以便预测中国在海底光缆生态系统中的行为,其重要性将在不久的将来倍增。

DNS隧道可以实现 DoH和DoT

via: https://github.com/net4people/bbs/issues/30

dnstt是一种新的DNS隧道,可以与DNS over HTTPS和DNS over TLS解析器一起使用,根据Turbo Tunnel的理念设计。

https://www.bamsoftware.com/software/dnstt/

git clone https://www.bamsoftware.com/git/dnstt.git

它与其他DNS隧道有何不同?

    它可以与DNS over HTTPS(DoH)和DNS over TLS(DoT)解析器一起使用,这使得网络观察者更难以判断是否使用了隧道。
    它嵌入了一个适当的可靠性和会话协议(KCP+smux)。客户端和服务器可以同时发送和接收数据,客户端无需等待一个查询接收到响应后再发送下一个查询。同时进行多个查询有助于提高性能。(这就是Turbo Tunnel的概念。)
    它使用Noise协议对隧道进行端到端的加密和认证,与DoH/DoT加密分开。

.------.  |            .--------.               .------.
|tunnel|  |            | public |               |tunnel|
|client|<---DoH/DoT--->|resolver|<---UDP DNS--->|server|
'------'  |c           '--------'               '------'
   |      |e                                       |
.------.  |n                                    .------.
|local |  |s                                    |remote|
| app  |  |o                                    | app  |
'------'  |r                                    '------'


这样的DNS隧道对于绕过审查是有用的。想象一下,一个审查者可以观察到客户端⇔解析器的连接,但无法观察到解析器⇔服务器的连接(图中的垂直线)。传统基于UDP的DNS隧道通常被认为很容易被检测到,因为它们生成的DNS消息的格式不同寻常,而且每个DNS消息必须带有隧道服务器的域名标记,因为中间的递归解析器需要知道将它们转发到哪里。但是使用DoH或DoT,客户端⇔解析器的DNS消息是加密的,因此审查者不能轻易地看到正在使用隧道。(当然,根据加密流量的数量和时序可能仍然可能启发式地检测到隧道,仅仅加密本身并不能解决这个问题。)

我希望这个软件发布可以展示这种类型的隧道设计的潜力。目前,该软件不提供TUN/TAP网络接口,甚至不提供SOCKS或HTTP代理接口。它只是将本地TCP套接字连接到远程TCP套接字。不过,您可以相对容易地设置它以像普通的SOCKS或HTTP代理一样工作,见下文。
DNS区设置

DNS隧道通过使隧道服务器充当特定DNS区的权威解析器来工作。中间的解析器通过将该区域的子域的查询转发到隧道服务器来充当代理。要设置DNS隧道,您需要一个域名和一个可以运行服务器的主机。

假设您的域名是example.com,您的主机的IP地址是203.0.113.2和2001:db8::2。转到您的域名注册商的配置面板,并添加三个新记录:

A    tns.example.com    指向203.0.113.2
AAAA    tns.example.com    指向2001:db8::2
NS    t.example.com    由tns.example.com管理

tns和t标签可以是任何您想要的内容,但tns标签不应是t标签的子域(该子域下的所有内容都保留给隧道负载)。t标签应该很短,因为DNS消息中的空间有限,而且域名占用其中的一部分。
隧道服务器设置

在服务器主机上运行以下命令;即在上面的示例中的tns.example.com / 203.0.113.2 / 2001:db8::2上运行。

cd dnstt-server
go build

首先,您需要为端到端隧道加密生成加密密钥。

./dnstt-server -gen-key -privkey-file server.key -pubkey-file server.pub
privkey写入server.key
pubkey写入server.pub

现在运行服务器。127.0.0.1:8000是将转发隧道流的TCP地址(图中的“远程应用程序”)。

./dnstt-server -udp :5300 -privkey-file server.key t.example.com 127.0.0.1:8000

隧道服务器需要在端口53上可访问。您可以直接绑定到端口53(-udp :53),但这需要您以root身份运行服务器。最好像上面显示的那样在非特权端口上运行服务器,并使用端口转发将端口53转发到它。在Linux上,以下命令将端口53转发到端口5300:

sudo iptables -I INPUT -p udp --dport 5300 -j ACCEPT
sudo iptables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
sudo ip6tables -I INPUT -p udp --dport 5300 -j ACCEPT
sudo ip6tables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300

您还需要为隧道服务器连接到的内容提供一些内容。它可以是代理服务器或其他任何内容。为了测试,您可以使用Ncat监听器:

sudo apt install ncat
ncat -lkv 127.0.0.1 8000

隧道客户端设置

cd dnstt-client
go build

将服务器上的server.pub(公钥文件)复制到客户端。您不需要在客户端上使用server.key(私钥文件)。

选择一个DoH或DoT解析器。这里有一个DoH解析器的列表:

    https://github.com/curl/curl/wiki/DNS-over-HTTPS#publicly-available-servers

以及这里有一个DoT解析器的列表:

    https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Public+Resolvers#DNSPrivacyPublicResolvers-DNS-over-TLS%28DoT%29
    https://dnsencryption.info/imc19-doe.html

要使用DoH解析器,请使用-doh选项:

./dnstt-client -doh https://doh.example/dns-query -pubkey-file server.pub t.example.com 127.0.0.1:7000

对于DoT,请使用-dot:

./dnstt-client -dot dot.example:853 -pubkey-file server.pub t.example.com 127.0.0.1:7000

127.0.0.1:7000指定了隧道的客户端端口。连接到该端口的任何内容(图中的“本地应用程序”)将通过解析器进行隧道传输,并连接到隧道服务器上的127.0.0.1:8000。您可以使用Ncat客户端测试它;运行此命令,您在客户端终端中键入的任何内容都将显示在服务器上,反之亦然。

ncat -v 127.0.0.1 7000

如何创建标准代理

您可以通过使隧道服务器转发到标准代理服务器来使隧道工作像普通的代理服务器。我发现使用Ncat的HTTP代理服务器模式很方便。

ncat -lkv --proxy-type http 127.0.0.1 3128
./dnstt-server -udp :5300 -privkey-file server.key t.example.com 127.0.0.1:3128

在客户端上,将您的应用程序配置为使用隧道的本地端口(127.0.0.1:7000)作为HTTP/HTTPS代理:

./dnstt-client -doh https://doh.example/dns-query -pubkey-file server.pub t.example.com 127.0.0.1:7000
curl -x http://127.0.0.1:7000/ https://example.com/

我尝试使用Firefox通过DNS隧道连接到Ncat HTTP代理,它可以正常工作。
本地测试

如果您只想看看它是如何工作的,而不想费心设置DNS区域或网络服务器,您可以在本地主机上运行隧道的两端。这种方式使用明文UDP DNS,所以不用说,跨互联网使用这样的配置是不隐蔽的。因为在这种情况下没有中间解析器,您可以使用任何您想要的域名;只需在客户端和服务器上保持一致即可。

./dnstt-server -gen-key -privkey-file server.key -pubkey-file server.pub
./dnstt-server -udp 127.0.0.1:5300 -privkey-file server.key t.example.com 127.0.0.1:8000
ncat -lkv 127.0.0.1 8000

./dnstt-client -udp 127.0.0.1:5300 -pubkey-file server.pub t.example.com 127.0.0.1:7000
ncat -v 127.0.0.1 7000

当它工作时,您将在服务器上看到如下的日志消息:

2020/04/20 01:48:58 pubkey 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff
2020/04/20 01:49:00 begin session 468d274a
2020/04/20 01:49:03 begin stream 468d274a:3

以及在客户端上看到如下的日志消息:

2020/04/20 01:49:00 MTU 134
2020/04/20 01:49:00 begin session 468d274a
2020/04/20 01:49:03 begin stream 468d274a:3

注意事项

对于外部观察者来说,DoH或DoT隧道是隐蔽的,但对于中间的解析器来说并非如此。如果解析器想要阻止您使用隧道,他们可以很容易地做到,只需不递归解析隧道服务器的DNS区域的请求。然而,隧道仍然对恶意解析器的窃听或篡改是安全的;解析器可以拒绝服务,但无法更改或读取隧道的内容。

出于技术原因,该隧道要求解析器支持至少1232字节的UDP负载大小,这比DNS保证的最小值512要大。我怀疑大多数公共的DoH或DoT服务器都满足这个要求,但我没有进行过调查或其他任何操作。

我没有进行任何系统性能测试,但我对Google、Cloudflare和Quad9解析器进行了一些初步测试。使用Google和Cloudflare时,通过Ncat传输文件时,我可以获得超过100 KB/s的下载速度。Cloudflare的DoH解析器偶尔会发送“400 Bad Request”响应(当隧道客户端看到这样的意外状态码时,它会自动限制自身的速度)。Quad9解析器的性能似乎明显不如其他解析器,但我不知道原因。