<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>未分类 &#8211; ZhiZhi</title>
	<atom:link href="https://zhizhi.date/archives/category/uncategorized/feed" rel="self" type="application/rss+xml" />
	<link>https://zhizhi.date</link>
	<description></description>
	<lastBuildDate>Sun, 17 Mar 2024 16:46:10 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.7.2</generator>
	<item>
		<title>使用官方脚本一键安装docker&#124;安装docker-compose</title>
		<link>https://zhizhi.date/archives/48.html</link>
					<comments>https://zhizhi.date/archives/48.html#respond</comments>
		
		<dc:creator><![CDATA[eswan]]></dc:creator>
		<pubDate>Sun, 17 Mar 2024 16:46:10 +0000</pubDate>
				<category><![CDATA[未分类]]></category>
		<guid isPermaLink="false">https://eswan.cc/?p=48</guid>

					<description><![CDATA[可在此命令后附带--mirror参数设置镜像源，以提高国内服务器下载docker的速度 注意是 docker  [&#8230;]]]></description>
										<content:encoded><![CDATA[
<pre class="wp-block-code"><code>curl -fsSL https://get.docker.com | bash -s docker</code></pre>



<p>可在此命令后附带<code>--mirror</code>参数设置镜像源，以提高国内服务器下载docker的速度</p>



<p>注意是 <code>docker compose</code> 而不是 <code>docker-compose</code>. 执行命令时候也没有这个杠</p>



<p>启动一个Debian系统的Docker</p>



<pre class="wp-block-code"><code>docker run -d --restart always --name mfcloud-all \
  -p 80:80 \
  -p 443:443 \
  -p 6606:6606 \
  -p 6379:6379 \
  debian:stable tail -f /dev/null
</code></pre>



<p>进入容器</p>



<pre class="wp-block-code"><code>docker exec -it mfcloud-all bash
</code></pre>



<p>安装常用工具</p>



<pre class="wp-block-code"><code>apt-get update
apt-get install wget curl procps -y

</code></pre>



<p>一键启动Portainer界面</p>



<pre class="wp-block-code"><code>docker run -d --name portainerUI -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer</code></pre>
]]></content:encoded>
					
					<wfw:commentRss>https://zhizhi.date/archives/48.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>缓解假墙伪墙攻击勒索的多种技术手段</title>
		<link>https://zhizhi.date/archives/44.html</link>
					<comments>https://zhizhi.date/archives/44.html#respond</comments>
		
		<dc:creator><![CDATA[eswan]]></dc:creator>
		<pubDate>Wed, 06 Mar 2024 21:22:00 +0000</pubDate>
				<category><![CDATA[未分类]]></category>
		<guid isPermaLink="false">https://eswan.cc/?p=44</guid>

					<description><![CDATA[我们知道，墙封锁一个网站有DNS污染、IP封锁、TCP Reset（TCP连接重置）等手段。而一个网站一旦被墙 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>我们知道，墙封锁一个网站有DNS污染、IP封锁、TCP Reset（TCP连接重置）等手段。而一个网站一旦被墙，一般情况下是无法直接通过301（或302）跳转到其他网站的。如果只是IP被封还好说，换IP通常能解决问题。但如果是根据域名关键字进行的TCP Reset，这时候不管怎么换IP（除非是国内IP）都无法解除封锁，当然也不可能进行301跳转（浏览器在收到HTTP服务器的301跳转Response之前TCP连接就已经被墙Reset而断开了，浏览器根本收不到HTTP服务器的任何Response）。而DNS污染的话自然更不用多说，只能换域名了，301跳转更不可能做到。然而，现在出现了很多号称可以解决域名被墙的服务，可以在网站被墙后通过301跳转到新的网站上。经过测试，还真能做到绕过墙的TCP Reset封锁，而这些服务的IP却都在海外（并非是使用了国内IP避免被墙的原因），而客户端只需要一个正常的浏览器即可（即客户端并不需要开启科学上网）。那么它们是怎么做到的呢？</p>



<p>要解释清楚其中的技术原理，还得回到2010年的<a href="https://zh.wikipedia.org/wiki/%E8%A5%BF%E5%8E%A2%E8%AE%A1%E5%88%92">西厢计划</a>。很早就经常科学上网的同学们应该都对<a href="https://zh.wikipedia.org/wiki/%E8%A5%BF%E5%8E%A2%E8%AE%A1%E5%88%92">西厢计划</a>并不陌生，它是一个只需要运行在客户端就能绕过很多封锁访问目标网站的工具，解决TCP Reset的原理是对本地的TCP/IP协议进行修改，在不伤害客户端和服务器之间的TCP连接的前提下让墙误以为TCP连接已经断开或者无法正确跟踪到TCP连接。之后出现的<a href="https://github.com/seclab-ucr/INTANG">INTANG项目</a>同样是这个想法的延续。</p>



<p>不过，不管是<a href="https://zh.wikipedia.org/wiki/%E8%A5%BF%E5%8E%A2%E8%AE%A1%E5%88%92">西厢计划</a>还是<a href="https://github.com/seclab-ucr/INTANG">INTANG</a>，都是运行在客户端上的工具，理论上只在服务器上运行无法起到效果，经过测试也能看到实际和理论相符。那么有没有一种工具可以在只服务器上运行，修改TCP/IP协议从而绕过封锁的工具呢？这方面同样有团队做了研究，研究的成果就是<a href="https://github.com/Kkevsterrr/geneva">Geneva项目</a>，<a href="https://gfw.report/">GFW Report</a>也对其做了<a href="https://gfw.report/blog/gfw_esni_blocking/zh/">详细介绍</a>。在<a href="https://gfw.report/blog/gfw_esni_blocking/zh/">这篇文章</a>中，列举了6种可以绕过TCP Reset的规则，6种规则都可以在只客户端部署生效（这时候服务器并不需要运行<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>），而前4种可以在只服务器部署生效（这时候客户端并不需要运行<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>）。不过<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>的官方Github中只收录了客户端的规则，<a href="https://gfw.report/blog/gfw_esni_blocking/zh/">文章</a>中的服务器规则并没有被收录在<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>的官方Github中。而且<a href="https://gfw.report/blog/gfw_esni_blocking/zh/">文章</a>中的策略3只给出了客户端的规则，遗漏了服务器端的规则。经过阅读<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>的规则介绍和策略3的描述，我已经重新还原了策略3的服务器规则，重新收录了4种服务器规则到我自己的<a href="https://github.com/lehui99/geneva/blob/master/strategies_server.md">Github Fork</a>中。经过本地环境的模拟加上tcpdump抓包观察测试，看到还原的策略3服务器规则和<a href="https://gfw.report/blog/gfw_esni_blocking/zh/">文章</a>中描述的行为一致，可以认为就是策略3本来的服务器规则。但是，在之后的真实环境的测试中发现这4种服务器策略全都失效了（不管HTTP还是HTTPS都已失效），墙依然对TCP进行了Reset。经过抓包看到服务器的行为确实和<a href="https://gfw.report/blog/gfw_esni_blocking/zh/">文章</a>中描述一致，所以可以确认并非是由于<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>没有正常工作导致的，而是墙已经为了应对这4种策略进行进化了。所以，墙并不是一成不变的，而是会进化的，那我们又该怎么办呢？</p>



<p>讲到这里，就不得不提另一个策略发现工具<a href="https://github.com/seclab-ucr/SymTCP">SymTCP</a>了。虽然现有的4种策略已经失效，但并不代表我们不能发现新的策略。而<a href="https://github.com/seclab-ucr/SymTCP">SymTCP</a>就是新策略发现工具，通过自动学习可以自动发现新的策略绕过墙的TCP Reset。之后我们就能把新的策略转换为<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>的规则格式进行使用了。不过，这样的话我们就会陷入到和墙的无休止争斗中，不断发现新策略，而墙则不断封锁新策略。而且规则的转换也是一个麻烦事，暂时还没有工具可以自动从<a href="https://github.com/seclab-ucr/SymTCP">SymTCP</a>的规则转换为<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>的规则，需要人工转换。并且需要修改<a href="https://github.com/seclab-ucr/SymTCP">SymTCP</a>使其不仅可以发现客户端规则同样也能发现服务器端规则。</p>



<p>那么，有没有一种一劳永逸的方法，使墙再怎么进化也无法避免这种策略的影响，而且这种策略只需要运行在服务器上，从而绕过封锁呢？在下结论之前，我们需要来研究一下一个正常的HTTP协议通讯是怎么进行的：</p>



<ol class="wp-block-list">
<li>浏览器发起TCP连接，经过3步（次）握手建立和服务器的TCP连接。</li>



<li>浏览器发送HTTP Request。</li>



<li>服务器收到Request，发送HTTP Response。</li>
</ol>



<p>而墙通常在看到浏览器发送的HTTP Request中包含关键字就会进行TCP Reset。讲到这里，聪明的同学或许已经想到了：如果服务器不等HTTP Request，而在TCP连接建立后立即发送HTTP Response，在墙进行TCP Reset之前就将Response送到浏览器进行抢答，是不是就能绕过TCP Reset了？而且还能无视之后墙的进化（因为浏览器的请求根本还没有经过墙）？说干就干，由于抢答模式不符合HTTP规范，所以常见的HTTP服务器无法实现抢答模式，所以让我们写个Python小程序来测试一下：</p>



<pre class="wp-block-code"><code>import socket
import threading
import time


def main():
    serv_sock = socket.socket()
    serv_sock.bind(('0.0.0.0', 80))
    serv_sock.listen(50)

    # 关闭Nagle算法，立即发送数据
    serv_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

    # 不等待，立即关闭连接
    serv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))

    while True:
        cli_sock, _ = serv_sock.accept()

        try:
            cli_sock.sendall(b'''HTTP/1.1 302 Moved Temporarily\r\n'''
                b'''Content-Type: text/html\r\n'''
                b'''Content-Length: 0\r\n'''
                b'''Connection: close\r\n'''
                b'''Location: https://www.microsoft.com/\r\n\r\n''')
        except Exception:  # 防止客户端提前关闭连接抛异常
            pass

        def wait_second():
            time.sleep(1)  # 等待1秒钟，确保数据发送完毕
            cli_sock.close()

        threading.Thread(target=wait_second).start()


if __name__ == '__main__':
    main()</code></pre>



<p>写完了来测试一下，发现依旧被TCP Reset了。那么，问题出在哪里？让我们重新回到上述HTTP协议通讯的3个步骤中的第1步——TCP的3步握手：</p>



<figure class="wp-block-image"><img decoding="async" src="https://raw.githubusercontents.com/lehui99/articles/main/img/01.png" alt="TCP三步握手+数据"/></figure>



<p>从TCP的3步握手中，我们可以看到第3步中客户端发送了ACK就已经完成了TCP连接的建立，这时候客户端并不需要再等服务器的回复就能立即发送数据。也就是说，浏览器会在发送ACK后立即发送HTTP Request，ACK和HTTP Request几乎是同时发出的。而服务器在收到浏览器的ACK后基本也就代表着已经收到了HTTP Request了，抢答失败！</p>



<p>那么，有没有办法让浏览器在TCP连接建立后延迟发送HTTP Request，而又不改动客户端行为呢？讲到这里，对TCP协议比较熟悉的同学或许已经想到了，那就是TCP window size。而通过调用<code>setsockopt()</code>就能修改TCP window size。让我们来修改一下Python小程序，把window size改为1再进行测试（TCP连接建立完成后，客户端只能发送1个字节，等待服务器的确认后才能继续发送更多的数据）：</p>



<pre class="wp-block-code"><code>import socket
import struct
import threading
import time


def main():
    serv_sock = socket.socket()
    serv_sock.bind(('0.0.0.0', 80))
    serv_sock.listen(50)

    # 设置TCP window size为1
    serv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1)

    # 不等待，立即关闭连接
    serv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))

    # 关闭Nagle算法，立即发送数据
    serv_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

    while True:
        cli_sock, _ = serv_sock.accept()

        try:
            cli_sock.sendall(b'''HTTP/1.1 302 Moved Temporarily\r\n'''
                b'''Content-Type: text/html\r\n'''
                b'''Content-Length: 0\r\n'''
                b'''Connection: close\r\n'''
                b'''Location: https://www.microsoft.com/\r\n\r\n''')
        except Exception:  # 防止客户端提前关闭连接抛异常
            pass

        def wait_second():
            time.sleep(1)  # 等待1秒钟，确保数据发送完毕
            cli_sock.close()

        threading.Thread(target=wait_second).start()


if __name__ == '__main__':
    main()</code></pre>



<p>改完测试，发现在Linux下仍旧被TCP Reset了（但在Windows下成功跳转了）。什么原因？通过抓包，我们看到对TCP window size的修改并没有生效，window size依旧很大。在查阅了<a href="https://man7.org/linux/man-pages/man7/socket.7.html">Linux man page</a>后我们看到关于<code>SO_RCVBUF</code>有这么一段话：</p>



<pre class="wp-block-code"><code>The minimum (doubled) value for this option is 256.
</code></pre>



<p>这也就意味着即使我们通过<code>setsockopt()</code>将<code>SO_RCVBUF</code>设置为1，Linux内核也会将其作为256处理（Windows却没有这个限制）。而<a href="https://man7.org/linux/man-pages/man7/socket.7.html">Linux man page</a>中也对<code>SO_RCVBUFFORCE</code>做了说明：只能突破最大值的限制（<code>but the rmem_max limit can be overridden</code>），但不能突破最小值的限制。而256字节基本就可以容纳整个HTTP Request。看来通过<code>setsockopt()</code>行不通（不管<code>SO_RCVBUF</code>还是<code>SO_RCVBUFFORCE</code>都行不通），Linux下我们得找别的方法。</p>



<p>讲到这里，我们很自然地又想到了<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>：上述<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>的策略2中服务器规则正是利用了TCP window size做到的四字节分割（设置window size为4）。这样，就绕过了<code>setsockopt()</code>的限制，直接对TCP数据包进行修改了。</p>



<p>在我们把四字节分割法部署到服务器运行<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>后，再结合上述Python小程序，经过测试我们发现已经成功绕过了TCP Reset，浏览器跳转到了微软网站。我们终于成功了！</p>



<p>然而，在浏览器第二次访问服务器时发现依然被TCP Reset了。不过，这已经影响不到301跳转（上述Python小程序还是302跳转，需要301的同学自行修改）了，301跳转的话浏览器已经被重定向到新的网站了，不会再次访问这个服务器（需要保证新旧网站不能使用相同IP），但这并不妨碍我们继续探究一下为什么第二次访问会被TCP Reset：通过抓包我们看到，第一次访问时浏览器虽然在第一个附带用户数据的数据包中只发送了4个字节，但后续会将剩余的整个HTTP Request通过一个数据包发送到服务器导致TCP Reset。而墙是有审查残留的，一段时间（几分钟）内不管是否出现关键字，对源IP和目标IP之间的TCP连接会进行无差别的Reset。所以在之后的这段审查残留时间内，只要TCP连接建立就会被Reset，抢答模式无法起到作用。</p>



<p>知道了原因我们就能采取对策了，我们知道客户端是因为收到了服务器确认数据包中的TCP window size很大，所以才能一次性把剩余的Request发送完毕，所以需要对后续的TCP window size做同样的修改，保证客户端看到的window size一直处于比较小的水平：通过对TCP协议的了解，我们知道连接建立时的window size是通过SYN+ACK包确定的，而后续的window size是通过ACK或PSH+ACK包确定的。所以，我们对规则2做少许的修改就能做到对后续window size的修改：</p>



<pre class="wp-block-code"><code>&#91;TCP:flags:A]-tamper{TCP:window:replace:1}-|
&#91;TCP:flags:PA]-tamper{TCP:window:replace:1}-|</code></pre>



<p>在服务器上我们同时运行规则2和上述修改后的2条规则（需要开3个<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>进程，注意第2、第3个进程需要在命令行中指定&#8211;in-queue-num和&#8211;out-queue-num避免和第1个冲突），我们终于能稳定地运行上述抢答模式，再也不会被TCP Reset了。</p>



<p>实际上我们可以将3条规则中的window size都设置得更小一些，甚至设置为0，避免客户端发送任何数据（实际上由于window size探测机制的原因，客户端仍旧会以极慢的速度一个字节一个字节地发送数据，不过不影响我们的抢答模式）：</p>



<pre class="wp-block-code"><code>&#91;TCP:flags:SA]-tamper{TCP:window:replace:0}-|
&#91;TCP:flags:A]-tamper{TCP:window:replace:0}-|
&#91;TCP:flags:PA]-tamper{TCP:window:replace:0}-|</code></pre>



<p>至此，HTTP的抢答模式就基本完成了。至于海外301跳转的那些服务可以同时服务于多个网站，原理也很简单：它们的名称虽然都是301跳转，但实际上并不一定必须使用301跳转——以上Python小程序可以修改为通过HTTP 200返回一个正常的HTML页面，其中嵌入一个JavaScript，在JavaScript中就能判断浏览器的网址进行条件跳转了。至于跳转规则，那大家就能在JavaScript中充分发挥自己的想象了。另外，由于<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>和上述小程序都是用Python编写的（甚至都没有使用asyncio），性能会比较差一些。<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>会自己添加iptables的NFQUEUE规则，不过规则太过于宽松，导致不需要处理的数据包也会经过<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>，并且会有规则覆盖的问题。所以大家需要在启动<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>后手动删除这些规则，自行添加更精确的规则（3条iptables规则需要分别设置成只处理OUTPUT链中TCP 80端口的SYN+ACK、ACK和PSH+ACK，避免条件重叠）。另外需要注意的是<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>区分客户端模式还是服务器模式，在服务器上运行的话需要加上<code>--server-side</code>参数。不过经过我的测试，即使使用精确的iptables规则也差不多只能利用20Mbps左右的带宽。如果希望有更高的性能，可以使用C/C++（结合libev，或asio，或直接epoll；或者使用golang、rust等）结合<a href="https://www.netfilter.org/projects/libnetfilter_queue/index.html">libnetfilter_queue</a>，利用iptables的NFQUEUE来完成，可以跑满千兆带宽。其实<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>的底层用的也是<a href="https://www.netfilter.org/projects/libnetfilter_queue/index.html">libnetfilter_queue</a>和NFQUEUE。由于本系列只做概念验证，而且因为篇幅的限制（本文已经很长了），在此就不展开C/C++的实现了，感兴趣的同学可以和我联系，如果感兴趣的同学比较多的话我就再新开一个系列讲一下这方面的内容。另外需要注意的是，<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>的编译环境为Python 3.6，使用Debian 10自带的Python 3.8/3.9会出现各种莫名其妙的问题，建议大家还是从Python官网下载Python 3.6的源码进行编译使用<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>。嫌麻烦的同学如果不想折腾<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>也可以将服务器换成Windows系统，可以直接使用上述Python小程序完成抢答模式。</p>



<p>在解决了HTTP的TCP Reset问题后，我们还需要解决HTTPS的TCP Reset。而HTTPS由于需要完成TLS握手才能发送HTTP Response，所以抢答模式似乎无法应用于HTTPS.</p>



<p>我们知道，HTTPS是需要客户端和服务器之间完成TLS握手后才能收发HTTP Request和Response。然而，墙是在TLS握手时通过SNI中的域名信息进行了TCP Reset，或<a href="https://gfw.report/blog/gfw_esni_blocking/zh/">通过ESNI头进行TCP阻断</a>，这时候还没有到能发送HTTP Request或Response的这个阶段，所以HTTP Response抢答模式无法应用于HTTPS。那么，我们该怎么办呢？</p>



<p>让我们回到TCP Reset本身——既然是TCP Reset，那么它只会对TCP协议生效。如果有一个应用层协议，其底层不是TCP呢？相信聪明的同学已经想到了，那就是HTTP 3.0（简称HTTP/3或h3）。H3的底层协议是QUIC，而QUIC是基于UDP而非TCP的。经过我的测试，发现墙现在无法识别QUIC协议，不管QUIC协议中出现任何敏感词都能无障碍过墙。这也是为什么有些网站（如v2ex）在解决了DNS污染（比如客户端主动加hosts）后，并且连接过一次（首次还是需要科学上网，原因之后会讲）后，就能关闭科学上网访问了。也就是说，使用了H3后，我们甚至不需要进行301跳转，直接就能无障碍访问服务器了（但仍建议使用301跳转，否则可能会使封锁升级，如DNS污染）。虽然H3现在仍旧处于草案阶段，但各大浏览器都已经进行了支持，而其中Google Chrome对H3的支持是最好的。</p>



<p>那么，如何在服务器上部署并开启H3呢？由于H3现在仍是草案阶段，所以Nginx的正式版并不支持H3，需要更换为<a href="https://hg.nginx.org/nginx-quic">Nginx-QUIC</a>来支持H3。编译使用<a href="https://hg.nginx.org/nginx-quic">Nginx-QUIC</a>也可以参考<a href="https://zhuanlan.zhihu.com/p/345660441">这篇文章</a>。另外，Cloudflare也已经支持了H3，可以自行开启（在“网络”设置中打开“HTTP/3（使用 QUIC）”）。在Cloudflare中开启H3后，Cloudflare和服务器的通讯仍旧可以使用HTTP/2（简称h2）或HTTP/1.1，并不需要服务器支持H3，而是由Cloudflare进行协议转换。</p>



<p>H3主要是依赖<code>Alt-Svc</code>这个HTTP头来进行协议选则的。比如我们可以添加HTTP头：</p>



<pre class="wp-block-code"><code>Alt-Svc: h3=":443"; ma=86400, h3-29=":443"; ma=86400, h3-28=":443"; ma=86400, h3-27=":443"; ma=86400
</code></pre>



<p>指出服务器支持H3，H3的UDP端口为443，有效期（过了有效期后浏览器又会重新使用H2或HTTP/1.1进行访问）为1天（86400秒），支持最新的H3草案以及27、28、29草案。另外，我们也可以灵活地修改<code>Alt-Svc</code>——比如可以在“<code>:443</code>”之前添加IP或域名，做到HTTP和HTTPS使用不用的IP或域名，方便我们在自己的服务器上部署HTTP抢答模式，又能使用Cloudflare的H3协议转换，或者使用不同的域名从而在原域名被DNS污染的情况下老用户（没有过有效期86400秒的）依然可以通过H3访问服务器（因为H3是另一个域名，而不是被DNS污染的那个域名。或者直接使用IP，从根本上杜绝了DNS污染，只不过之后有可能遭到IP封锁）。另外，有效时间也可以进行适当延长（比如从1天延长到1个月或更长时间），避免客户端尝试H2或HTTP/1.1并且延长老用户的过期时间。我们可以在还没遇到HTTPS的TCP Reset时就开启H3，这样即使之后遭遇了HTTPS的TCP Reset，曾经访问过网站的老用户在H3有效期内也能继续访问网站。而且我们也不必担心UDP数据包被ISP丢弃（俗称UDP被QoS）的问题，因为浏览器在H3连接失败的时候会快速回退到H2和HTTP/1.1。</p>



<p>然而，H3是一个Alternative服务。首次访问服务器时，浏览器并不会主动使用H3，还是会优先使用H2或HTTP/1.1。当获取到<code>Alt-Svc</code>头后，浏览器才会在之后的访问中优先使用H3。这也是为什么有些网站（如v2ex）需要在科学上网的情况下访问过一次后才能关闭科学上网进行访问（当然，DNS污染首先还是需要用户自行修改hosts解决）。那么，我们如何让用户全程不使用科学上网的情况下访问服务器呢？</p>



<p>首先想到的是通过上面提到的HTTP抢答模式提供<code>Alt-Svc</code>头，不过可惜的是现在的主流浏览器会忽略HTTP中的<code>Alt-Svc</code>头，只接受HTTPS中的<code>Alt-Svc</code>头。而如果HTTPS本来就已经被TCP Reset的话，浏览器就无法获取<code>Alt-Svc</code>头了。那么，那些提供HTTPS的301海外跳转服务是怎么做的呢？</p>



<p>在调查和尝试了几个支持HTTPS的301海外跳转服务后，我们发现，它们根本就没有解决HTTPS的TCP Reset问题，HTTPS依然被TCP Reset了，而它们宣称支持HTTPS中301跳转的做法就是在HTTP抢答模式下加上普通的TLS服务。因为大部分网站遭遇的只是HTTP中的TCP Reset而HTTPS并不会被TCP Reset，所以只需要解决HTTP的301跳转，再加上普通的HTTPS，表面上就能同时做到HTTP和HTTPS的301跳转。而且在调查过程中我们还发现有个跳转服务主页的HTTPS也被TCP Reset了，而他们自己却对此毫无办法。那么，对于新用户访问HTTPS的TCP Reset，我们也只能止步于此，束手无策了吗？那也未必。</p>



<p>其实，从本系列一开始，我们就假设301跳转服务器是在国外。如果使用的是国内服务器，那么就能避免墙的识别了（因为不过墙）。但使用国内服务器（似乎）有个绕不过去的坎：备案系统——使用HTTP会提示域名没有备案，使用默认端口443的HTTPS同样会有TCP Reset。我们又该怎么办？其实，备案系统其实就是一个简化版的墙，没有TCP流量重组的功能，使用上面提到的抢答模式同样也能绕过备案系统。而且正是因为备案系统没有TCP流量重组的功能，我们甚至可以在TCP模式的HTTP和HTTPS中设置TCP window size（比如Linux上可以使用<a href="https://github.com/Kkevsterrr/geneva">Geneva</a>；Windows上可以自行编写一个反向代理，在其中设置<code>SO_RCVBUF</code>为1，两者的用法在上面都已进行说明，在此不再重复），从而可以直接通过HTTP和HTTPS绕过备案系统而无需使用301跳转（因为备案系统没有TCP流量重组功能，所以只需要在连接初始时设置个较小的TCP window size，之后恢复正常即可。这也是很多国内免备案服务器的原理和使用的方案）。不过，绕过备案系统展示网页有一定的风险，301跳转风险会小一些，希望大家还是要权衡好利弊。</p>



<p>在讨论好HTTPS中防TCP Reset的方案后，最后让我们来聊一聊DNS污染。</p>



<p>其实，在撰写本文之前，我曾去尝试过几个声称可以解决域名污染的301海外跳转的服务，但无一例外都失败了，都无法解决DNS污染。然后我也去咨询了提供了这些服务的人，他们的说法大致分为两种：</p>



<ol class="wp-block-list">
<li>需要将被DNS污染的域名的NS记录指向国内的DNS服务（如DNSPod、阿里云等），然后需要等一段时间，运气好的话过一段时间就会解封了（对于这种说法，我也曾经亲自验证过，将一个被DNS污染的域名的NS记录转移回国内，等了几个月，依然被污染）。</li>



<li>域名污染指的是域名被关键字Reset，而不是DNS污染（这个说法和大多数人理解的不同，将域名污染解释为了TCP Reset，和DNS污染分为了两个概念），他的服务只能解决域名污染，不能解决DNS污染。</li>
</ol>



<p>那么，对于DNS污染，我们只能束手无策，或只能碰运气转移回国内了吗？那也未必。不过，由于解决DNS污染所需要的成本较高，所以这也是为什么之前H3虽然能让用户在原有域名下继续访问，我仍旧建议使用301跳转的原因。否则封锁升级为DNS污染后连301跳转都会变得比较困难了。</p>



<p>讲到这里，细心的同学应该已经发现，其实在刚才H3的使用方法中，已经介绍了如何使老用户在DNS污染的情况下继续进行访问的方法了。这是解决DNS污染部分问题的方法之一。那么，还有没有别的方法也能解决部分问题呢？其实，在H3的方案中，我们主要利用了浏览器的Cache中记录了H3的服务信息，来让老用户通过不同的域名或IP进行访问的。那么，浏览器的Cache中除了能保存H3的信息外，也是可以保存其他内容的。讲到这里，聪明的同学应该已经想到了。没错，就是<code>Cache-Control</code>（或使用<code>Expires</code>也有相同的效果）。通过这个HTTP头，我们可以将一个页面的过期时间设置成很长，在过期之前，浏览器并不会发起HTTP请求，甚至没有网络的离线情况下都能访问（使用F5刷新除外，这时候浏览器会忽略过期时间从而发起HTTP请求）。在这个页面中，我们可以引用别的域名下的JavaScript脚本文件，在JavaScript而非HTML中渲染整个网页。这样，老用户同样可以在DNS污染的情况下继续访问我们的服务器。不过，这种做法对SEO不是很友好，但我们可以使用HTML和JavaScript同时渲染的方法让搜索引擎可以进行索引——HTML中仍旧是正常内容给搜索引擎进行索引，而浏览器会加载JavaScript，使用JavaScript重新渲染一遍网页，避免Cache没有过期而呈现老页面的问题。老用户的问题可以解决，但新用户怎么办呢？或者我们有没有办法从根本上来解决DNS污染呢？而且听说现在有一些价格昂贵的污染清洗服务，它们真的能从根本上解决DNS污染吗？它们是怎么做的呢？</p>



<p>如果我们想从根本上解决问题，首先我们还需要了解整个DNS系统是怎么工作的：</p>



<ol class="wp-block-list">
<li>DNS服务器分为递归查询服务器、DNS代理和权威服务器（称为ADNS）。我们把递归查询服务器和DNS代理统称为LDNS。</li>



<li>普通用户上网所使用的一般是ISP提供的LDNS，它会负责向ADNS查询真实的A（和AAAA以及其它）记录。</li>



<li>ADNS即是域名的NS记录所指向的服务器。</li>
</ol>



<p>我们知道，DNS污染是墙在海外ADNS返回正确的结果之前进行了抢答，返回了错误的结果。这样，在国内LDNS向海外ADNS查询的时候，同样会受到DNS污染，从而返回给普通用户错误的结果。那么，我们有没有办法劫持ISP的LDNS，从而让其返回我们想要的IP而不是墙返回的错误IP呢？这样，虽然DNS污染仍旧存在，但普通用户却得到了正确的IP，从而可以正常访问我们的服务器了。</p>



<p>讲到这里，就不得不提到2008年曾经轰动全球的<a href="https://zhuanlan.zhihu.com/p/92899876">DNS投毒攻击案</a>了。在<a href="https://zhuanlan.zhihu.com/p/92899876">这篇文章</a>中，Kaminsky可以修改任意LDNS中缓存的A（或AAAA以及其它）记录，虽然在经过了那次事件后这个漏洞更难被利用了，但终究无法完全修复，我们仍旧可以利用其中的原理劫持ISP的LDNS（能猜中源端口和QID就能进行劫持），将被污染域名的IP换成自己想要的IP。而且由于墙污染的TTL较小，我们也能更快地利用这个漏洞而不需要每次等待1天的时间。所以短则几天，慢则几个星期就能劫持成功。这也是现在有些价格不菲的污染清洗服务所采用的方案之一。当然，某些攻击团队也同样在利用这个漏洞就行DNS劫持，虽然导致的结果是LDNS被劫持而非墙的DNS污染，但对于普通用户所造成的结果是一致的——网站无法访问。不过，要实施这种DNS劫持需要源IP欺骗，现在能进行源IP欺骗的服务器已经越来越少了。那么我们还有没有别的方法无需源IP欺骗来劫持LDNS呢？</p>



<p>既然大家已经看了上面的<a href="https://zhuanlan.zhihu.com/p/92899876">文章</a>，那么让我们来重新细致地梳理一下整个DNS查询过程。以浏览器访问 https://www.youtube.com/ 为例：</p>



<ol class="wp-block-list">
<li>操作系统向ISP的LDNS发起请求 www.youtube.com 的A（以及AAAA）记录。</li>



<li>LDNS查询缓存中有没有 www.youtube.com 的A（或AAAA）记录，如有则返回给客户端，如没有则执行第3条。</li>



<li>LDNS查询（可从缓存中查询）DNS根服务器（当前为13个）的A（或AAAA）记录。</li>



<li>LDNS向根服务器（或从缓存中）查询 .com 的ADNS。</li>



<li>LDNS向 .com 的ADNS发起查询 youtube.com 的ADNS（即 youtube.com 的NS记录）。</li>



<li>LDNS向 youtube.com 的ADNS发起查询 www.youtube.com 的A（或AAAA）记录。之后返回给客户端。</li>
</ol>



<p>我们知道， youtube.com 是个被DNS污染的域名，所以第5和第6步会受到墙的DNS污染，而前4步不会。第6步我们也很熟悉了，国内IP向国外IP发起查询请求时墙就会抢答 www.youtube.com 的错误A（或AAAA）记录。而第5步中，由于LDNS在国内，查询到 youtube.com 的NS记录同样会受到污染。我们同样知道，墙的DNS污染虽然成功概率接近100%，但仍有很小的概率会污染失败。那么我们能不能不停地向LDNS请求 www.youtube.com 的A（或AAAA）记录，在墙污染失败的时候，LDNS就能刷新到正确的IP地址了呢？可惜的是，LDNS是有缓存的，在缓存有效期内，不会再次向ADNS发起请求。即使在缓存失效后偶尔会由于污染失败得到了正确的IP地址，但在缓存再次失效后由于污染再次回到了错误的IP地址。所以被污染的概率仍旧接近100%。墙看上去似乎无懈可击，我们该怎么办呢？</p>



<p>让我们重新回到第5条，使用国内IP向 .com 的ADNS请求 youtube.com 的NS记录。以Linux为例：</p>



<pre class="wp-block-code"><code>dig ns youtube.com @e.gtld-servers.net
</code></pre>



<p>（<code>e.gtld-servers.net</code>为 .com 的其中一个ADNS）<br>我们看到墙返回了污染的结果， youtube.com 被污染的NS是……咦？不对！墙竟然返回的是A（或AAAA）记录，而不是我们查询的NS记录！而且墙的污染是有很小的概率会失败的！相信聪明的同学已经想到了——由于墙返回的是不是NS记录，所以LDNS没有获取到 youtube.com 的NS记录，自然无法将 youtube.com 的NS记录存入缓存中。所以，在下次客户端请求 youtube.com 的NS记录时，LDNS会再次向 .com 的ADNS请求 youtube.com 的NS记录而不是从缓存中获取。既然不存在缓存，我们就能一直向LDNS发起请求，而LDNS就会一直向ADNS发起请求，直到墙的污染失败出现，LDNS终于获得了正确的NS记录。而由于NS记录本身是带有TTL的，所以会被存入LDNS的缓存之中，在缓存过期之前不会再受到墙的污染。而我们可以将NS记录的TTL设置得非常长，从而可以在很长得时间内让墙得污染无法生效。而在TTL过期之后，我们可以利用同样的方法再次让LDNS获得正确的NS记录。</p>



<p>在解决了第5条中的污染后，我们还需要解决第6条中的污染。而第6条中墙返回的确实是查询的A（或AAAA）记录，会被存入LDNS缓存，也就无法利用上述方法了。我们该怎么办呢？相信聪明的同学也已经想到了。没错，就是将NS记录转移回国内，这样DNS请求就不会过墙，自然就不会受到污染了。可是，不对呀？刚才不是讲过我也曾经亲自验证过，将一个被DNS污染的域名的NS记录转移回国内，等了几个月，依然被污染么？那是因为之前的测试只是将NS记录指向了国内服务器，我们并没有大量地发送NS查询请求到LDNS，所以LDNS并没有获得正确的NS记录，所以污染仍旧存在。而且，ISP的LDNS是分运营商并且分区域的。只将一个LDNS中的NS刷新到正确结果只能解决一个运营商的一小片区域中的污染，如果想要在全国范围内解决污染，需要使用大量的IP地址（因为很多ISP的LDNS限制了查询请求的发起IP只能是本地宽带用户），不停地对大量的LDNS查询NS记录，直到全国大部分地区的LDNS都获取到了正确的NS记录，才能在大范围内解决DNS污染。而且即使LDNS获取到了正确的NS记录，查询仍然要继续，因为缓存是有过期时间的。而这，也是现在很多昂贵的污染清洗服务所采用的方案之一。</p>



<h1 class="wp-block-heading" id="参考资料">参考资料</h1>



<ul class="wp-block-list">
<li><a href="https://github.com/lehui99/articles">https://github.com/lehui99/articles</a></li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://zhizhi.date/archives/44.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>宝塔 Linux 开心版</title>
		<link>https://zhizhi.date/archives/41.html</link>
					<comments>https://zhizhi.date/archives/41.html#respond</comments>
		
		<dc:creator><![CDATA[eswan]]></dc:creator>
		<pubDate>Fri, 22 Dec 2023 20:02:40 +0000</pubDate>
				<category><![CDATA[未分类]]></category>
		<guid isPermaLink="false">https://eswan.cc/?p=41</guid>

					<description><![CDATA[Centos安裝命令（默認安裝是 7.9.10 直接在線升級 8.0.4） 試驗性Centos/Ubuntu/ [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p><strong>Centos安裝命令（默認安裝是 7.9.10 直接在線升級 8.0.4）</strong></p>



<pre class="wp-block-code"><code>yum install -y wget &amp;&amp; wget -O install.sh http://io.bt.sy/install/install_6.0.sh &amp;&amp; sh install.sh</code></pre>



<p><strong>試驗性Centos/Ubuntu/Debian安裝命令 獨立運行環境（py3.7） 可能存在少量兼容性問題 不斷優化中</strong></p>



<pre class="wp-block-code"><code>curl -sSO http://io.bt.sy/install/install_panel.sh &amp;&amp; bash install_panel.sh</code></pre>



<p><strong>Ubuntu Deepin安裝命令</strong></p>



<pre class="wp-block-code"><code>wget -O install.sh http://io.bt.sy/install/install-ubuntu_6.0.sh &amp;&amp; sudo bash install.sh</code></pre>



<p><strong>Debian安裝命令</strong></p>



<pre class="wp-block-code"><code>wget -O install.sh http://io.bt.sy/install/install-ubuntu_6.0.sh &amp;&amp; bash install.sh</code></pre>



<p><strong>Fedora安裝命令</strong></p>



<pre class="wp-block-code"><code>wget -O install.sh http://io.bt.sy/install/install_6.0.sh &amp;&amp; bash install.sh</code></pre>



<p><strong>Linux面闆 8.0.4 升級企業版命令 1（所有官方版 / 開心版 包括低版本 都可以執行這個升級到 8.0.4 開心版）</strong></p>



<pre class="wp-block-code"><code>curl https://io.bt.sy/install/update_panel.sh|bash</code></pre>



<p><strong>Linux面闆 8.0.4 升級企業版命令 2（所有官方版 / 開心版 包括低版本 都可以執行這個升級到 8.0.4 開心版）</strong></p>



<pre class="wp-block-code"><code>curl http://io.bt.sy/install/update6.sh|bash</code></pre>
]]></content:encoded>
					
					<wfw:commentRss>https://zhizhi.date/archives/41.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>如何把Markdown文档渲染成Vue组件</title>
		<link>https://zhizhi.date/archives/37.html</link>
					<comments>https://zhizhi.date/archives/37.html#respond</comments>
		
		<dc:creator><![CDATA[eswan]]></dc:creator>
		<pubDate>Tue, 19 Dec 2023 15:04:00 +0000</pubDate>
				<category><![CDATA[未分类]]></category>
		<guid isPermaLink="false">https://eswan.cc/?p=37</guid>

					<description><![CDATA[实现步骤 1.新建一个组件 这个简单就不详细说，只要定义好一个div，class命名为markdown-bod [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">实现步骤</h2>



<h3 class="wp-block-heading"><a href="https://github.com/maidivh/hai-27.github.io/blob/master/page/%E5%A6%82%E4%BD%95%E6%8A%8AMarkdown%E6%96%87%E6%A1%A3%E6%B8%B2%E6%9F%93%E6%88%90Vue%E7%BB%84%E4%BB%B6.md#1%E6%96%B0%E5%BB%BA%E4%B8%80%E4%B8%AA%E7%BB%84%E4%BB%B6"></a>1.新建一个组件</h3>



<p>这个简单就不详细说，只要定义好一个div，class命名为markdown-body就好，至于为什么是这个，后面再说。</p>



<pre class="wp-block-code"><code>&lt;template>
  &lt;div class="markdown-body">
    
  &lt;/div>
&lt;/template>
&lt;script>
export default {
  name: "MyMarkdown",
  data() {
    return {
      md: ""
    };
  }
&lt;/script>
&lt;style>
.markdown-body {
  box-sizing: border-box;
  margin: 0 auto;
  padding: 0 40px;
}
&lt;/style></code></pre>



<h3 class="wp-block-heading"><a href="https://github.com/maidivh/hai-27.github.io/blob/master/page/%E5%A6%82%E4%BD%95%E6%8A%8AMarkdown%E6%96%87%E6%A1%A3%E6%B8%B2%E6%9F%93%E6%88%90Vue%E7%BB%84%E4%BB%B6.md#2-%E8%8E%B7%E5%8F%96markdown%E6%96%87%E4%BB%B6"></a>2. 获取Markdown文件</h3>



<p>首先需要考虑一个问题，既然想把Markdown文档渲染成组件，那Markdown文件从哪里来？</p>



<p>方法很多，我就说两种：</p>



<p><strong>静态资源导入</strong></p>



<p>像外部css文件一样导入，需要使用loader，可以看一下 <a href="https://www.npmjs.com/package/markdown-loader" target="_blank" rel="noreferrer noopener">markdown-loader</a>，我没有使用这个方法，就不细说了，感兴趣的同学，自己了解一下。</p>



<p><strong>远程获取</strong></p>



<p>比如从静态资源服务器、后端服务器等远程服务器获取。</p>



<p>我为什么要选择这种方法呢？</p>



<p>我主要考虑到，README.md不是一成不变的，我可能随时需要修改。如果使用第一种方法，每次修改都需要对项目重新build，然后重新部署，很麻烦。但使用第二种方法，文件修改了，只需要把文件重新上传到服务器就可以了，服务器都不需要重启。</p>



<p>我的后端服务器在之前就已经实现静态资源请求的处理，你在在线Demo中看到的图片，绝大部分都是从后端服务器获取来的。所以我现在只需要在后端服务器的public文件夹下新建一个docs文件夹，然后把README.md放进去。前端在组件创建完成后，使用axios向后端发起请求，就能获取到README.md。</p>



<p>代码：</p>



<pre class="wp-block-code"><code>created() {
  // 从后端请求README.md
  this.$axios
    .get("/api/public/docs/README.md", {})
    .then(res => {
      this.md = res.data;
    })
    .catch(err => {
      return Promise.reject(err);
    });
}</code></pre>



<p>先在控制台输出看一下获取回来的数据是怎样的</p>



<p>这个是不是很熟悉，就是README.md源文件，那如何渲染到组件呢？</p>



<h3 class="wp-block-heading"><a href="https://github.com/maidivh/hai-27.github.io/blob/master/page/%E5%A6%82%E4%BD%95%E6%8A%8AMarkdown%E6%96%87%E6%A1%A3%E6%B8%B2%E6%9F%93%E6%88%90Vue%E7%BB%84%E4%BB%B6.md#3-%E6%8A%8Amarkdown%E6%96%87%E6%A1%A3%E6%B8%B2%E6%9F%93%E6%88%90html"></a>3. 把Markdown文档渲染成html</h3>



<p>Markdown文档拿到了，那怎样把它渲染到页面呢，v-html吗？显然不行，我找到了 <a href="https://www.npmjs.com/package/vue-markdown" target="_blank" rel="noreferrer noopener">vue-markdown </a>，一个 Markdown解析器，可以把Markdown文档解析成html。</p>



<p><strong>安装</strong></p>



<pre class="wp-block-code"><code>npm install --save vue-markdown
</code></pre>



<p><strong>导入</strong></p>



<pre class="wp-block-code"><code>import VueMarkdown from "vue-markdown";
</code></pre>



<p><strong>局部注册组件</strong></p>



<pre class="wp-block-code"><code>components: {
  VueMarkdown
}</code></pre>



<p><strong>使用</strong></p>



<pre class="wp-block-code"><code>&lt;div id="my-markdown" class="markdown-body">
  &lt;vue-markdown :source="md">&lt;/vue-markdown>
&lt;/div></code></pre>



<p><strong>效果</strong></p>



<p>先看一下效果</p>



<p>是不是觉得有点丑，不急，没有css样式，当然是有点丑。</p>



<h3 class="wp-block-heading"><a href="https://github.com/maidivh/hai-27.github.io/blob/master/page/%E5%A6%82%E4%BD%95%E6%8A%8AMarkdown%E6%96%87%E6%A1%A3%E6%B8%B2%E6%9F%93%E6%88%90Vue%E7%BB%84%E4%BB%B6.md#4-css%E6%A0%B7%E5%BC%8F"></a>4. CSS样式</h3>



<p>css样式从哪里？自己写吗，当然不啦，作为一名优秀的**，有现成的，绝不自己写，于是我找到了 <a href="https://www.npmjs.com/package/github-markdown-css" target="_blank" rel="noreferrer noopener">github-markdown-css</a> ，为了使用方便，我直接下载下来，放在“../assets/css/”目录下，然后导入。</p>



<pre class="wp-block-code"><code>@import "../assets/css/github-markdown.css";
</code></pre>



<p>然而，事情并没有像想象的那么美好，页面什么都没有改变。</p>



<p>我看了一下github-markdown-css的文档，才知道要在父div添加class：<strong>markdown-body</strong>，至于为什么，大家应该都懂，就不说了。</p>



<p>看一下效果吧</p>



<h3 class="wp-block-heading"><a href="https://github.com/maidivh/hai-27.github.io/blob/master/page/%E5%A6%82%E4%BD%95%E6%8A%8AMarkdown%E6%96%87%E6%A1%A3%E6%B8%B2%E6%9F%93%E6%88%90Vue%E7%BB%84%E4%BB%B6.md#%E6%80%BB%E7%BB%93"></a>总结</h3>



<p>到这里，把Markdown文档渲染成Vue组件的需求已经实现，需求很简单，实现也很简单，没有什么技术含量，就当学习的过程分享一下吧。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://zhizhi.date/archives/37.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>开启Nginx提供预压缩文件</title>
		<link>https://zhizhi.date/archives/35.html</link>
					<comments>https://zhizhi.date/archives/35.html#respond</comments>
		
		<dc:creator><![CDATA[eswan]]></dc:creator>
		<pubDate>Sat, 16 Dec 2023 12:08:52 +0000</pubDate>
				<category><![CDATA[未分类]]></category>
		<guid isPermaLink="false">https://eswan.cc/?p=35</guid>

					<description><![CDATA[]]></description>
										<content:encoded><![CDATA[
<pre class="wp-block-code"><code>server {
    listen 80;
    listen 443 ssl http2;
    server_name test.com;
    index index.php index.html index.htm default.php default.htm default.html;
    root /www/wwwroot/test.com;

    # ...省略了SSL和其他配置...

    # 开启gzip_static
    gzip_static on;

    # ...省略了其余配置...

    location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
        expires 30d;
        error_log /dev/null;
        access_log /dev/null;
    }

    location ~ .*\.(js|css)?$ {
        expires 12h;
        error_log /dev/null;
        access_log /dev/null;
        # 也可以在特定的location块中开启gzip_static
        # gzip_static on;
    }

    # ...省略了日志和其他配置...
}
</code></pre>
]]></content:encoded>
					
					<wfw:commentRss>https://zhizhi.date/archives/35.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>NSFW检测公共API</title>
		<link>https://zhizhi.date/archives/12.html</link>
					<comments>https://zhizhi.date/archives/12.html#respond</comments>
		
		<dc:creator><![CDATA[eswan]]></dc:creator>
		<pubDate>Sun, 03 Dec 2023 10:58:48 +0000</pubDate>
				<category><![CDATA[未分类]]></category>
		<guid isPermaLink="false">https://eswan.cc/?p=12</guid>

					<description><![CDATA[因为自己维护了一个公开图床，但是由于发现有人大批量上传色情图片用来卖片，遂开始寻找方便且高性能的NSFW检测服 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>因为自己维护了一个公开图床，但是由于发现有人大批量上传色情图片用来卖片，遂开始寻找方便且高性能的NSFW检测服务，现在使用Python实现了高性能的NSFW检测服务。</p>



<p>图床地址 <a href="https://imgimg.cc/" target="_blank" rel="noreferrer noopener">https://imgimg.cc/</a></p>



<p>NSFW检测API地址 <a href="https://nsfw-api.imgimg.cc/image" data-type="link" data-id="https://nsfwjs.imgimg.cc/image" target="_blank" rel="noreferrer noopener">https://nsfw-api.imgimg.cc/image</a></p>



<p>参数名：img<br>一次只能提交一张图片，限定格式：jpg png webp</p>



<p>下方是检测API判断标准，只要符合NSFW判断标准则会返回结果，porn的值越大表示含有NSFW可能性越大，结果为0并不表示不含有NSFW内容，检测算法相对宽松，也可能存在没有识别到的，如果需要非常严格的检测，建议使用商业API。<br>本API使用了项目 <a href="https://github.com/notAI-tech/NudeNet" data-type="link" data-id="https://github.com/notAI-tech/NudeNet" target="_blank" rel="noreferrer noopener">https://github.com/notAI-tech/NudeNet</a></p>



<pre class="wp-block-code"><code>检测到NSFW内容
{
    "porn": 0.6853373050689697
}
检测到二维码（固定值0.9）
{
    "porn": 0.9
}
未检测到NSFW内容
{
    "porn": 0
}</code></pre>



<figure class="wp-block-table is-style-regular"><table><thead><tr><th class="has-text-align-center" data-align="center">类别</th><th class="has-text-align-center" data-align="center">是否为NSFW</th></tr></thead><tbody><tr><td class="has-text-align-center" data-align="center">FEMALE_GENITALIA_COVERED</td><td class="has-text-align-center" data-align="center">否</td></tr><tr><td class="has-text-align-center" data-align="center">FACE_FEMALE</td><td class="has-text-align-center" data-align="center">否</td></tr><tr><td class="has-text-align-center" data-align="center">BUTTOCKS_EXPOSED</td><td class="has-text-align-center" data-align="center">是</td></tr><tr><td class="has-text-align-center" data-align="center">FEMALE_BREAST_EXPOSED</td><td class="has-text-align-center" data-align="center">是</td></tr><tr><td class="has-text-align-center" data-align="center">FEMALE_GENITALIA_EXPOSED</td><td class="has-text-align-center" data-align="center">是</td></tr><tr><td class="has-text-align-center" data-align="center">MALE_BREAST_EXPOSED</td><td class="has-text-align-center" data-align="center">否</td></tr><tr><td class="has-text-align-center" data-align="center">ANUS_EXPOSED</td><td class="has-text-align-center" data-align="center">是</td></tr><tr><td class="has-text-align-center" data-align="center">FEET_EXPOSED</td><td class="has-text-align-center" data-align="center">否</td></tr><tr><td class="has-text-align-center" data-align="center">BELLY_COVERED</td><td class="has-text-align-center" data-align="center">否</td></tr><tr><td class="has-text-align-center" data-align="center">FEET_COVERED</td><td class="has-text-align-center" data-align="center">否</td></tr><tr><td class="has-text-align-center" data-align="center">ARMPITS_COVERED</td><td class="has-text-align-center" data-align="center">否</td></tr><tr><td class="has-text-align-center" data-align="center">ARMPITS_EXPOSED</td><td class="has-text-align-center" data-align="center">否</td></tr><tr><td class="has-text-align-center" data-align="center">FACE_MALE</td><td class="has-text-align-center" data-align="center">否</td></tr><tr><td class="has-text-align-center" data-align="center">BELLY_EXPOSED</td><td class="has-text-align-center" data-align="center">否</td></tr><tr><td class="has-text-align-center" data-align="center">MALE_GENITALIA_EXPOSED</td><td class="has-text-align-center" data-align="center">是</td></tr><tr><td class="has-text-align-center" data-align="center">ANUS_COVERED</td><td class="has-text-align-center" data-align="center">否</td></tr><tr><td class="has-text-align-center" data-align="center">FEMALE_BREAST_COVERED</td><td class="has-text-align-center" data-align="center">否</td></tr><tr><td class="has-text-align-center" data-align="center">BUTTOCKS_COVERED</td><td class="has-text-align-center" data-align="center">否</td></tr></tbody></table></figure>



<p>下面是完整的Python代码：</p>



<p>在使用之前先安装所需依赖</p>



<pre class="wp-block-code"><code>pip install Flask pyzbar Pillow nudenet
</code></pre>



<pre class="wp-block-code"><code>from flask import Flask, request, jsonify
from nudenet import NudeDetector
from pyzbar.pyzbar import decode
from PIL import Image
import imghdr
import tempfile

app = Flask(__name__)
nude_detector = NudeDetector()


@app.route('/image', methods=&#91;'POST'])
def upload_file():
    file = request.files.get('img')

    # 检查是否存在文件或文件名为空
    if not file or file.filename == '':
        return jsonify({'porn': 0}), 200

    # 使用临时文件处理
    with tempfile.NamedTemporaryFile(delete=False) as temp_file:
        file.save(temp_file.name)
        file_format = imghdr.what(temp_file.name)

        if file_format not in &#91;'jpeg', 'png', 'webp']:
            return jsonify({'porn': 0}), 200

        # 如果不需要检测二维码，可以删除下面的检查
        image = Image.open(temp_file.name)
        if decode(image):
            return jsonify({'porn': 0.9}), 200

        detections = nude_detector.detect(temp_file.name)
        nsfw_classes = {'BUTTOCKS_EXPOSED', 'FEMALE_BREAST_EXPOSED', 'FEMALE_GENITALIA_EXPOSED', 'ANUS_EXPOSED',
                        'MALE_GENITALIA_EXPOSED'}
        max_score = max((detection&#91;'score'] for detection in detections if detection&#91;'class'] in nsfw_classes),
                        default=0)

    # 临时文件在退出with块时自动删除
    return jsonify({'porn': max_score}), 200


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=1106)
</code></pre>
]]></content:encoded>
					
					<wfw:commentRss>https://zhizhi.date/archives/12.html/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
