自定义 golang 仓库的 module / 库名

<p>如果你写过 golang,那你应该能在导入库时,注意到库名大多都是以 <code>github.com</code> 开头,跟一个链接差不多,复制到浏览器里也能直接访问。但有时也能看到一些与众不同的库名,例如 <a href="https://pkg.go.dev/gopkg.in/yaml.v3"><code>gopkg.in/yaml.v3</code></a> 和 <a href="https://pkg.go.dev/go.uber.org/zap"><code>go.uber.org/zap</code></a></p> <p>你可以点开上面的两个链接看看,就会发现它们在 <a href="https://pkg.go.dev/">pkg.go.dev</a> 中指向的代码仓库都托管在 GitHub,而它们却又不使用 <code>github.com/user/repo</code> 这样的库名,这是怎么做到的呢</p> <h2>大概的流程</h2> <p>golang 对这个实现方法在文档中有一个段落,可见:<a href="https://pkg.go.dev/cmd/go#hdr-Remote_import_paths"><code>cmd/go</code> #Remote import paths</a></p> <h3>来自 GitHub 的仓库</h3> <p>简要的来说,当你在终端里运行 <code>go get</code> 或 <code>go install</code> 命令时,golang 会尝试去解析你传入的库名,如果库名是 <code>github.com/&lt;user&gt;/&lt;repo&gt;</code> 这样,它可以直接识别到这是一个 GitHub 仓库,就会走一套标准的流程,检查它是不是一个 golang 库,且这个库名与代码仓库的 <code>go.mod</code> 文件中定义的 <code>module</code> 名称相符,顺利的话就可以正常下载了</p> <h4>如果不用定义的奇怪库名</h4> <p>你可能会好奇,如果类似 <code>gopkg.in/yaml.v3</code> 这样的库名,而我们知道它的代码仓库托管于 GitHub 中,我们能不能绕过这个奇怪名字,像普通托管在 GitHub 的 golang 项目那样下载它呢?</p> <pre><code class="language-bash"># 这里在加了后缀 /v3 是因为 gopkg.in/yaml.v3 本来就指向 v3 分支 $ go get github.com/go-yaml/yaml/v3 go: downloading github.com/go-yaml/yaml/v3 v3.0.1 go: github.com/go-yaml/yaml/v3@upgrade (v3.0.1) requires github.com/go-yaml/yaml/v3@v3.0.1: parsing go.mod: module declares its path as: gopkg.in/yaml.v3 but was required as: github.com/go-yaml/yaml/v3 </code></pre> <p>显而易见,并不行。通过 <code>go get</code> 或 <code>go install</code> 命令下载某个库时,它的库名必须与 <code>go.mod</code> 文件中定义的 <code>module</code> 名称相同,否则就是下不了</p> <h3>其他自建的版本控制服务</h3> <p>不来自 GitHub 也没关系,golang 也设计有一些回退方法,这时就会根据库名来判断像不像一个 URL,并尝试向这个 URL 发送 <code>HTTP</code> 请求,如果请求的是一个自建的 git 或其他版本控制服务的网页 URL,你的自部署服务大概率会正确的为 golang 提供信息,后续的流程就跟上面差不多了</p> <p>当然这种情况不多见,更多时候应该是在企业内部的使用场景?</p> <h3>自定义 golang 项目的库名</h3> <p>如果要自定义一个 golang 项目的库名,那我们最少需要一个可以存放 <code>HTML</code> 文件,支持 <code>HTTPS</code> 且可以公开访问的域名。用一些平台提供的二级域名也可以,至于代码仓库托管在哪都可以</p> <p>首先,你要确定你的库名是什么,它的开头应该是你的域名,然后跟着你的仓库名称,例如下方的演示</p> <pre><code class="language-text">我有一个 golang 仓库 https://gitea.trle5.xyz/trle5/tplate 不修改的话,库名就应该是 gitea.trle5.xyz/trle5/tplate 也就是去掉了开头的 https:// 你可以看到,它同时包含了二级域名,用户名和仓库名,这显得它又长又难记 同时还要避免冲突,最后决定的库名就是 trle5.xyz/gopkg/tplate </code></pre> <blockquote> <p>理论上也可以直接拿一整个二级域名作为库名,就是 <code>tplate.trle5.xyz</code> 这样,但我没试过具体行不行</p> </blockquote> <h4>修改原来的库名</h4> <p>如果你之前跟我一样不知道 golang 库名的命名规范,那你的库名大概就是一个单词,而且 golang 的拓展并没有提供一键重命名库名的方法... 于是,只能先修改 <code>go.mod</code> 文件开头的 <code>module</code> 名称,再看文件的报错逐个替换了</p> <p>注意:如果你之前发布过 <code>tag</code> 或 <code>release</code>,那你修改库名后,还要推送一个新的 <code>tag</code> 或 <code>release</code>,不然你在使用 <code>go get</code> 或 <code>go install</code> 时,它会默认获取 <code>latest</code> 版本的内容,修改库名后最新的一个 <code>commit</code> 版本并没有包含此次更改,就会出现类似 <a href="#如果不用定义的奇怪库名">前面</a> 的错误</p> <h4>创建包含特殊属性的 <code>&lt;meta&gt;</code> 标签</h4> <p>能让 golang 从一个 <code>HTML</code> 页面中识别仓库信息的关键在于其中的 <code>&lt;meta&gt;</code> 标签,golang 不关心这个 <code>HTML</code> 页面里有什么其他内容,只要有对应的信息就行了,像 GitHub 上的 golang 仓库默认都会包含这个标签和对应的属性,下方的仓库 URL 填写你存放代码仓库的网页链接,代码仓库中 <code>go.mod</code> 文件中的 <code>module</code> 名称必须跟你填写的这个库名相同</p> <pre><code class="language-html">&lt;meta name=&quot;go-import&quot; content=&quot;&lt;库名&gt; git &lt;仓库 URL&gt;&quot;&gt; </code></pre> <p>下面是一个完整的示例,我把它保存为一个名为 <code>tplate.html</code> 的文件,作为资源文件存放到了我的网站里,访问路径就是 <a href="https://trle5.xyz/gopkg/tplate.html"><code>trle5.xyz/gopkg/tplate</code></a></p> <pre><code class="language-html">&lt;!doctype html&gt; &lt;html&gt; &lt;head&gt; &lt;meta name=&quot;go-import&quot; content=&quot;trle5.xyz/gopkg/tplate git https://gitea.trle5.xyz/trle5/tplate.git&quot;&gt; &lt;meta name=&quot;go-source&quot; content=&quot;trle5.xyz/gopkg/tplate _ https://gitea.trle5.xyz/trle5/tplate/src/branch/custom{/dir} https://gitea.trle5.xyz/trle5/tplate/src/branch/custom{/dir}/{file}#L{line}&quot;&gt; &lt;/head&gt; &lt;style&gt;body { color: #c8c8b9; background-color: #14140f; } a {color: #b9d282;}&lt;/style&gt; &lt;body&gt; &lt;p&gt;go get trle5.xyz/gopkg/tplate&lt;/p&gt; &lt;a href=&quot;https://gitea.trle5.xyz/trle5/tplate&quot;&gt;https://gitea.trle5.xyz/trle5/tplate&lt;/a&gt; &lt;/body&gt; &lt;/html&gt; </code></pre> <p>里面其实很多东西都是多余的,这里是参照了 Gitea 的实现而修改的文件还加上了一些信息和链接,你可以编辑好 <code>&lt;meta&gt;</code> 标签,塞到任意一个 <code>HTML</code> 页面的 <code>&lt;head&gt;</code> 标签里都可以</p> <blockquote> <p>注意:<strong><code>go.mod</code> 文件</strong>、<strong><code>&lt;meta&gt;</code> 标签属性</strong> 和 <strong>对应 URL</strong> 的三个库名必须完全相同,即:</p> <ol> <li><code>go.mod</code> 中库名为 <code>example.com/package</code></li> <li>浏览器中 <code>example.com/package</code> 可正常访问</li> <li><code>example.com/package</code> 页面为 <code>HTML</code> 文档,且其中包含上方的 <code>&lt;meta&gt;</code> 标签</li> <li><code>&lt;meta&gt;</code> 标签中的属性已经 <a href="#创建包含特殊属性的--标签">按照需求</a> 正确填写了</li> </ol> </blockquote> <p>之后确保 CDN 缓存之类的不会干扰你的请求,就可以尝试使用新的库名来导入或安装你的项目了</p> <hr /> <p>2026-03-08 更新:</p> <p>由于想到要不直接往文章里塞 <code>&lt;meta&gt;</code> 标签就好了,这样也不用担心文章冲突,改了一下博客模板发现还算简单,于是就直接塞到文章里了</p> <p>但是似乎因为 trailing slash 的缘故,可能还需要改一改,例如目前 <code>trle5.xyz/tplate</code> 的信息实际上存储在 <code>/tplate/index.html</code></p> <p>但 Cloudflare Pages 自动把 <code>/tplate</code> 的请求重定向到 <code>/tplate/index.html</code>, pkg.go.dev 和 <code>go</code> 命令行也能正确抓取到信息,<del>先暂时不动它吧...</del> 改了</p>

2026/3/8
阅读更多

golang template 库使用教程

<blockquote> <p>这个文章目前是用来查看各种关键字和函数如何使用的,需要的话可以看看 <a href="/post/go-template-intro/">golang template 库入门教程</a> 文章</p> <p>文章里的描述可能与实际有出入,请参考官方文档 <a href="https://pkg.go.dev/text/template#section-documentation">text/template - Go Packages</a></p> </blockquote> <p>在我写 <a href="https://gitea.trle5.xyz/trle5/tplate">tplate</a> 时,我发现网上能找到的 golang template 教程都很分散,不是很方便,于是打算写一篇文章留给自己用</p> <h2>运算符</h2> <h3>逻辑运算符</h3> <table> <thead> <tr> <th align="center">关键字</th> <th>使用方法</th> <th>等价 go 语句</th> </tr> </thead> <tbody> <tr> <td align="center"><strong>and</strong></td> <td><code>and C1 C2</code></td> <td><code>C1 &amp;&amp; C2</code></td> </tr> <tr> <td align="center"><strong>or</strong></td> <td><code>or C1 C2</code></td> <td><code>C1 || C2</code></td> </tr> <tr> <td align="center"><strong>not</strong></td> <td><code>not C1</code></td> <td><code>!C1</code></td> </tr> </tbody> </table> <h3>比较运算符</h3> <table> <thead> <tr> <th align="center">关键字</th> <th>使用方法</th> <th>等价 go 语句</th> <th>备注</th> </tr> </thead> <tbody> <tr> <td align="center"><strong>eq</strong></td> <td><code>eq C1 C2</code></td> <td><code>C1 == C2</code></td> <td><b><ins>EQ</ins></b>ual</td> </tr> <tr> <td align="center"><strong>ne</strong></td> <td><code>ne C1 C2</code></td> <td><code>C1 != C2</code></td> <td><b><ins>N</ins></b>ot <b><ins>E</ins></b>qual</td> </tr> <tr> <td align="center"><strong>lt</strong></td> <td><code>lt C1 C2</code></td> <td><code>C1 &lt; C2</code></td> <td><b><ins>L</ins></b>ess <b><ins>T</ins></b>han</td> </tr> <tr> <td align="center"><strong>le</strong></td> <td><code>le C1 C2</code></td> <td><code>C1 &lt;= C2</code></td> <td><b><ins>L</ins></b>ess than or <b><ins>E</ins></b>qual</td> </tr> <tr> <td align="center"><strong>gt</strong></td> <td><code>gt C1 C2</code></td> <td><code>C1 &gt; C2</code></td> <td><b><ins>G</ins></b>reater <b><ins>T</ins></b>han</td> </tr> <tr> <td align="center"><strong>ge</strong></td> <td><code>ge C1 C2</code></td> <td><code>C1 &gt;= C2</code></td> <td><b><ins>G</ins></b>reater than or <b><ins>E</ins></b>qual</td> </tr> </tbody> </table> <p><strong><code>eq</code> 关键字可以接受多个参数,例如 <code>eq C1 C2 C3</code>,但等价的 go 语句其实是 <code>C1 == C2 || C1 == C3</code></strong></p> <h3>复杂条件式</h3> <p>如果要创建复杂的条件式,有时需要使用括号 <code>()</code> 将子条件包裹起来</p> <table> <thead> <tr> <th>条件式</th> <th>等价 go 语句</th> </tr> </thead> <tbody> <tr> <td><code>and C1 (not C2)</code></td> <td><code>C1 &amp;&amp; !C2</code></td> </tr> <tr> <td><code>and (and (gt C1 1) (gt C2 1)) (ne C1 C2)</code></td> <td><code>(C1 &gt; 1 &amp;&amp; C2 &gt; 1) &amp;&amp; C1 != C2</code></td> </tr> <tr> <td><code>eq C1 C2 C3 C4</code></td> <td><code>C1 == C2 || C1 == C3 || C1 == C4</code></td> </tr> </tbody> </table> <h2>关键字</h2> <h3><code>if else</code> 条件判断</h3> <p>可以在模板中判断一些条件,来控制要执行的数据</p> <pre><code class="language-js">{{ if .A }} {{/* 符合 .A 条件时的内容 */}} {{ else if .B }} {{/* 不符合 .A 但符合 .B 条件时的内容 */}} {{ else }} {{/* 不符合以上全部条件时的内容 */}} {{ end }} </code></pre> <h3><code>range</code> 遍历</h3> <p>使用 <code>range</code> 方法即可遍历数组、切片或 map 键值对,也可以在要遍历的对象为空时进行其他操作</p> <pre><code class="language-js">{{ range .Slice }} {{/* 此时 . 会指向循环中的数据 */}} 当前遍历的值: {{ . }} {{ else }} 这个切片是空的 {{ end }} </code></pre> <h4>在遍历中使用外部值</h4> <p>在 <code>range</code> 方法的作用域中,<code>.</code> 符号会指向当前遍历的数据,若要在循环中使用外部的值,需要添加 <code>$</code> 前缀</p> <pre><code class="language-js">{{/* 假设传入的数据中有 Slice 和 Value 这两个变量 */}} 这是 Value 的值: {{ .Value }} {{ range .Slice }} 当前遍历的值: {{ . }} 循环外部的值: {{ $.Value }} {{ end }} </code></pre> <h4>获取索引或键名</h4> <p>在使用 <code>range</code> 方法时可以通过创建变量来获取数组、切片或 map 键值对的索引或键</p> <pre><code class="language-js">{{ range $index, $data := .Slice }} index 变量: {{ $index }} data 变量: {{ $data }} 当前遍历的值: {{ . }} {{ end }} </code></pre> <p>创建了变量后依然可以通过 <code>.</code> 来直接调用当前遍历的数据</p> <h3><code>index</code> 获取单个元素</h3> <p>可以使用 <code>index</code> 方法来获取数组、切片或 map 键值对中的单个元素</p> <pre><code class="language-js">{{ $item_0_in_slice := index .Slice 0 }} 切片中的第 0 个元素为 {{ $item_0_in_slice }} {{ $item_abc_in_map := index .Map &quot;abc&quot; }} map 键对值中的 abc 元素为 {{ $item_abc_in_map }} </code></pre> <h3><code>break</code> <code>continue</code> 控制循环</h3> <p>在使用 <code>range</code> 方法遍历内容时,可以像 go 代码的 for 循环一样使用 <code>break</code> 和 <code>continue</code> 控制接下来的操作</p> <pre><code class="language-js">{{/* 假设 .Numbers 中的数据是 1,2,3,4,5,6,7,8,9,10 */}} {{ range .Numbers }} {{ if lt . 5 }} 数字小于 5: {{ . }} {{ continue }} {{ end }} {{ if gt . 2 }} 正在退出循环 {{ break }} {{ end }} {{ end }} 现在在循环外 </code></pre> <h3><code>with</code> 暂时修改 <code>.</code> 指向的数据</h3> <p>有时候只需要使用数据中的某个字段,可以用 <code>with</code> 命令来暂时修改 <code>.</code> 指向的数据</p> <pre><code class="language-js">{{/* 假设传入的数据结构为 .Post.Date */}} 现在是 {{ .Post.Date.Year }} 年 {{ .Post.Date.Month }} 月 {{ .Post.Date.Day }} 日 {{ with .Post.Date }} {{/* 此时 . 已经指向 .Post.Date,调用 .Year 同等于 .Post.Date.Year */}} 现在是 {{ .Year }} 年 {{ .Month }} 月 {{ .Day }} 日 {{ end }} </code></pre> <p>若要在 <code>with</code> 作用域中使用其他外部值,参考 <a href="#在遍历中使用外部值">在遍历中使用外部值</a></p> <p><code>with</code> 命令还可以像 <code>if else</code> 那样选择要使用的数据</p> <pre><code class="language-js">{{ with .Update.Date }} {{/* 此时 . 指向 .Update.Date */}} 更新时间 {{ .Year }} 年 {{ .Month }} 月 {{ .Day }} 日 {{ else with .Create.Date }} {{/* 此时 . 指向 .Create.Date */}} 创建时间 {{ .Year }} 年 {{ .Month }} 月 {{ .Day }} 日 {{ else }} {{/* 因为没有匹配上面的条件,此时 . 没有发生变化 */}} 未知时间 {{ end }} </code></pre> <h3><code>define</code> <code>template</code> 定义和调用模板</h3> <p>可以使用 <code>define</code> 命令来预先定义一个模板,再通过 <code>template</code> 命令来调用之前定义的模板</p> <pre><code class="language-js">{{ define &quot;A&quot; }} {{/* 模板 A 中的 . 为调用时传入的数据 */}} 在模板 A 中的数据是 {{ . }} {{ end }} 正在调用模板 A 并传入 &quot;text&quot; {{ template &quot;A&quot; &quot;text&quot; }} </code></pre> <h3><code>block</code> 重新定义模板并立即调用</h3> <p><code>block</code> 命令其实是一个<abbr title="某种用于简化代码阅读和表达的语法结构">语法糖</abbr>,它会定义一个模板并立即调用它</p> <pre><code class="language-js">{{/* 首先定义一个名为 template 的模板,它会输出传入的数据 */}} {{ define &quot;template&quot; }} 传入的数据为 {{ . }} {{ end }} {{/* 重新定义 template 模板并传入 . 来执行,它会先输出数据再接着提示语句 */}} {{ block &quot;template&quot; . }} {{ . }} 是传入的数据 {{ end }} {{/* 同等于下方的代码,但需要注意:在一个模板中不允许定义另一个模板 */}} {{ define &quot;template&quot; }} {{ . }} 是传入的数据 {{ end }} {{/* 调用重新定义后的 template 模板并传入 . 来执行*/}} {{ template &quot;template&quot; . }} </code></pre> <p>为了方便替换使用,go 的 <code>text/template</code> 和 <code>html/template</code> 库中的模板是允许重复定义的,实际的模板为最后被解析的那个</p>

2026/5/19
阅读更多

new tplate

Old Love / New Love - Twin Shadow

2025/11/20
阅读更多

为 Chrome OS 设置代理

想要在 Chrome OS 上简单方便的代理设备的流量,那就只能在 Android 环境下安装软件,再由系统设定好的本机通道自动在 Chrome、Android 与 Linux 虚拟机之间共享,好处自然是方便且稳定,代价嘛...

2024/8/6
阅读更多

在 Chrome OS 的用户终端 (crosh) 中使用 sudo 权限

<p>首先按 <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>F2</kbd> 打开 VT-2 终端,输入 <code>chronos</code> 用户进行登录,默认无密码</p> <pre><code class="language-bash"># 分别创建 ed25519 和 rsa 类型的密钥,两个都要用到 # 会询问一些信息,可以全部回车 ssh-keygen -t ed25519 ssh-keygen -t rsa # 将密钥复制到 sshd 服务器需要的目录 sudo mkdir -p /mnt/stateful_partition/etc/ssh/ sudo cp ~/.ssh/id_ed25519 /mnt/stateful_partition/etc/ssh/ssh_host_ed25519_key sudo cp ~/.ssh/id_rsa /mnt/stateful_partition/etc/ssh/ssh_host_rsa_key # 允许上方的公钥建立 ssh 连接 cat ~/.ssh/id_ed25519.pub &gt;&gt; ~/.ssh/authorized_keys cat ~/.ssh/id_rsa.pub &gt;&gt; ~/.ssh/authorized_keys </code></pre> <p>接下来,往 <code>~/.bashrc</code> 中预设几个命令,好让使用的过程更方便:</p> <pre><code class="language-bash">echo &quot;alias vt2port=\&quot;sudo /usr/sbin/sshd -p 6969\&quot;&quot; &gt;&gt; ~/.bashrc echo &quot;alias vt2connect=\&quot;ssh 127.1 -p 6969\&quot;&quot; &gt;&gt; ~/.bashrc # 使命令生效 source ~/.bashrc </code></pre> <p>到这里具体的配置就完成了,在 VT-2 中输入 <code>vt2port</code> 后,会自动开启 sshd 服务器,之后你就可以按 <kbd>Ctrl</kbd> + <kbd>D</kbd> 注销 VT-2 了,除非你重启 Chrome OS 或手动杀掉它,否则它都会一直运行</p> <p>然后按 <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>F1</kbd> 回到 Chrome OS 中,再按 <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>T</kbd> 打开 crosh 后,输入 <code>shell</code> 进入终端再输入 <code>vt2connect</code>,会询问你要不要信任主机,确认后就可以方便的在 crosh 中使用 sudo 命令了</p> <p>以后每次重新启动之后,开启 sshd 服务器需要的步骤就是:</p> <ol> <li>进入 VT-2 登录 <code>chronos</code> 用户</li> <li>输入 <code>vt2port</code> 开启 sshd 服务器</li> <li>注销 VT-2 返回 Chrome OS</li> </ol> <p>需要在 crosh 中使用 sudo 权限的步骤:</p> <ol> <li>打开 crosh &gt; 输入 <code>shell</code> 进入终端</li> <li>输入 <code>vt2connect</code> 连接到 sshd 服务器</li> <li>运行任何需要 sudo 的命令</li> </ol> <p>参考链接:<a href="https://github.com/oddbyte/howto-use-sudo-in-crosh">oddbyte/howto-use-sudo-in-crosh</a></p>

2024/6/20
阅读更多

在没有管理员权限的情况下更新 Windows 的系统时间

<p>有些机房的老电脑的 BIOS 电池早就没了电,还拿不到管理员权限来更改时间,通过更改策略组来给予其他用户更改时间的权限就可以调整到正确的时间了</p> <p>首先按下 <kbd>Win</kbd> + <kbd>R</kbd> 键进入运行窗口,在弹出的窗口里输入下方内容来进入 <strong>本地策略组编辑器</strong>:</p> <pre><code>gpedit.msc </code></pre> <p>然后根据路径找到对应的策略文件夹:</p> <pre><code>计算机配置 ↳ Windows 设置 ↳ 安全设置 ↳ 本地策略 ↳ 用户权限分配 &gt; 更改系统时间 </code></pre> <p>双击策略名称后,会弹出来它的属性,点击中下方的 <strong>添加用户或组(U)...</strong>,再点击下方的 <strong>高级(A)...</strong>,继续点击右侧的 <strong>立即查找(N)</strong> 按钮,就会在窗口下方显示现有的用户和组,选择要添加的用户或组点击保存即可。图标显示一个人的是用户,显示两个人的就是用户组</p> <p>考虑在机房可能并不能重启,若遇到这种情况,可以考虑添加其他普通用户或来宾用户,并给予修改时间的权限,再通过开始菜单切换到对应的用户来修改时间</p> <p>如果你使用的设备登录的是没有添加用户权限的用户嘛,那就给当前用户添加权限,保存后注销当前用户再登录,检查一下能不能修改时间。我自己试了试似乎不行,祝你好运吧</p>

2023/4/27
阅读更多

隐藏站点的源服务器 IP

<p>从不开 CDN,域名 DNS 直接解析到 IP,再到服务器被打到空路由后的总结</p> <h3>为什么要隐藏源服务器 IP</h3> <p>首先我们要看看浏览器访问一个站点时,这个请求会如何到达源服务器:</p> <ol> <li>浏览器获得用户输入的域名,首先向 DNS 服务器发送查询请求</li> <li>DNS 服务器收到了请求,解析到了域名对应的 IP,返回给浏览器</li> <li>浏览器得知了源服务器的 IP 地址,接着继续向源服务器发送请求</li> </ol> <p><b><abbr title="Domain Name System"><a href="https://zh.wikipedia.org/zh-cn/域名系统" target="_blank" >DNS</a></abbr></b> 的作用就不多说了,简单来看就是放域名进去,就可以吐出域名对应的 IP</p> <p>这个过程中,浏览器就已经知道源服务器的 IP 了,怎么知道的呢?当然是 DNS 告诉它的</p> <p>但 DNS 是怎么知道源服务器的 IP 呢?不是什么魔法,是站点主人自己告诉 DNS 的,不然没了 DNS,想访问站点只能记一串 IP,很不方便</p> <h4>现在我看某个人不爽,想把他的网站做掉</h4> <p><code>当然我不提倡任何网络攻击,这里只是做一个例子</code></p> <p>上面我们知道了请求是如何到达源站的,最简单的方法,就是一秒给他发几千个请求甚至更多,就能起效果。于是立刻手搓了一段代码,一秒发五千个请求,这个叫分布式拒绝服务(<b><abbr title="Distributed Denial-Of-Service">DDoS</abbr></b>)攻击</p> <p>由于服务器的配置并不是很高,处理不来我们发送的这么多请求,一段时候后,服务器的主人发现了不对劲,赶紧把 DNS 解析删除了,打算等我们不打他之后再重新添加 DNS 解析,让网站恢复访问</p> <p>此时,虽然 DNS 解析被删除了,但服主在被打之前在 DNS 里设置的 IP 已经被我们请求过了,我们早已知道了源站 IP。于是我们乘胜追击,把发送请求的域名换成 IP,接着打:</p> <pre><code class="language-text">在我们的不懈努力下,服务器的占用爆满,服主只能关机跑路 就算服主重新开机,迎接他的依然是 100% 占用,跑不掉 </code></pre> <p>到这里我们已经知道 DNS 与 IP 公开绑定的后果了,就算站点不能通过域名访问了,只要服务器不关机,那还是能通过 IP 访问的,不做些什么,站点就只能停了</p> <p>所以,我们要在确保通过域名可以正常访问的情况下,保证源服务器的 IP 不被泄露</p> <h3>有哪些办法可以隐藏源服务器 IP?</h3> <p>我们先代入到前面的攻击事件中服主的视角,看看有什么方法可恢复站点访问,还能让他打不到我们</p> <h4>换 IP</h4> <p>一台机器想要作为站点的服务器,大部分情况下都需要被分配一个可被外部访问的 IP 地址,但它是可以更换的,可能会需要一些成本</p> <p>更换了 IP 后,我们可以的服务器就可以开机了,因为攻击者并不知道我们服务器新的 IP 是多少,在不设定 DNS 解析的情况下,服务器是暂时安全了。但如果再次把域名解析到新的 IP,那他还是跟前面一样,只需要访问一次就能知道我们的新 IP,服务器就会继续挨打</p> <p>我们也可以更换 IP 的同时更换一个域名,不过这样可能会损失之前积累的用户。如果你将更换域名的消息告诉了之前的用户,而攻击者潜伏在用户之中,那么你的服务器可能会再次遭殃</p> <p>若网站之前有被搜索引擎收录,更换后的新域名再次被收录,那攻击者可以通过搜索引擎得知更换后的域名,DNS 再返回对应的 IP,那服务器同样会遭殃</p> <h4>上高防 / 升级服务器</h4> <p>只要你的配置够高,他就没法把你的服务器干掉线,只是看你的钱包能不能受得住。DDoS 的攻击和防御成本并不是一个量级的,一般的小站点可没有这个资金去买高防,升级服务器的性价比也很低,这是一个很糟糕的解决办法</p> <h4>使用 CDN</h4> <p>本文章的主角,能让 DNS 解析不会给出你的源站 IP,同时还能保证域名可以正常访问,它是怎么运作的呢?</p> <p><b><abbr title="Content Delivery Network">CDN</abbr></b> 翻译过来就是内容分发网络,在 DNS 层面,它可以将 DNS 解析出来的 IP 从你的源站 IP 换为来自 CDN 的 IP,避免 DNS 直接暴露源站 IP</p> <p><strong>DNS 解析出来不是源站 IP,那怎么获取我的站点内容呢?</strong></p> <p>按照流程,在 <a href="#为什么要隐藏源服务器-ip">前面</a> 所说的 DNS 解析出 IP 进行返回的过程中,使用 CDN 后,解析出来的 IP 是来自 CDN 的。接下来的请求部分,用户会从向源站 IP 发送请求变为向 CDN 的 IP 发送请求,这里 CDN 需要负责接受请求,然后再向源服务器发送请求,过程如下:</p> <pre><code class="language-text"># 没有使用 CDN 的请求 用户发送请求 &gt; 源服务器收到请求 源服务器发送响应 &gt; 用户收到响应 # 使用了 CDN 的请求 用户发送请求 &gt; CDN 收到请求 CDN 转发请求 &gt; 源服务器收到请求 源服务器发送响应 &gt; CDN 收到响应 CDN 转发响应 &gt; 用户收到响应 </code></pre> <p>可以看到,开启 CDN 比不开启 CDN,用户每个请求都会先到达 CDN,再由 CDN 转发到源服务器,源服务器处理完得到响应之后,再发送给 CDN,CDN 再将回应发送给用户</p> <p>看上去多了两个步骤,麻烦了不少,但我们需要的隐藏源站 IP 功能不是实现了?</p> <p>不过要注意,CDN 很多时候只能转发 HTTP/S 请求,例如你本来用你的域名直接解析到 IP,使用 git 克隆仓库时可以使用域名替换服务器 IP,但开启 CDN 后,就不能这样了,你可能需要找一些其他方法</p> <h4>还没完</h4> <p>我们选择了最实用的 CDN 方法来隐藏了源站 IP,但还有一些需要注意的事情</p> <ol> <li>如果你在没开 CDN 之前就被打了,只开 CDN 是不行的,你还需要同步更换 IP。CDN 只是阻断了通过 DNS 获取源站 IP 的方法,但你的 IP 早就在开 CDN 前泄露了,此时攻击者不会和你走流程,直接对着 IP 继续打,服务器还是得遭殃</li> <li>我没有开过 CDN,但我也并没有被打过,看了文章我现在感觉有必要开一个 CDN,还来得及吗?<br> 没问题,但有隐患。网上有很多专门扫域名扫 IP 的,可能已经记录了你的 IP,但情况并不坏,只要没人刻意去查,是找不到的,就算找到了,你简单换一下 IP,那些记录也就失效了</li> </ol> <h3>避免源服务器暴露站点特征</h3> <p>截止到这里,防止 DNS 泄露源站 IP 的方法其实已经解决了,不过为了安全,我们还是要配置一下源服务器上的反向代理,避免网上那些扫 IP 的服务暴露了我们在 CDN 后的源服务器 IP</p> <h4>排查暴露的端口</h4> <p>首先,我们要检查一下直接访问服务器 IP 能获得什么,请按照下面的列表排查一下自己服务器:</p> <ul> <li>在浏览器中通过 <code>IP:80</code> 或 <code>IP:443</code> 能否直接访问到你的 HTTP/S 服务</li> <li>如果你有其他非标准端口的服务,使用 <code>IP:端口</code> 是否能直接访问</li> <li>如果你有部署 docker 或其他容器类服务,请检查容器的端口映射是否暴露在了公网</li> <li>(可选)去 <a href="https://search.censys.io/">Censys</a> 搜搜你的 IP 中有哪些被记录的端口和服务</li> </ul> <h5>暴露的 80/443 标准端口</h5> <p>如果你的 HTTP/S 服务能够通过 80/443 端口访问,那说明你的服务就直接暴露出来了,就算攻击者因为 CDN 中继保护还不知道你的 IP,但 80/443 是标准的 HTTP/S 端口,最简单的 IP 扫描也不会放过这两个端口,只要被扫到,基本就会暴露对应的数据</p> <p>你可能会好奇,不是开了 CDN 吗?但 CDN 也是通过 80/443 端口才能转发服务器与客户端之间的请求,CDN 能访问到的数据,通过 IP 访问也可以,而直接通过 IP 访问则会绕过 CDN。如果一个 IP 访问后显示的网页数据和某个域名一样,那不用猜,这个 IP 就是域名后面对应的源服务器 IP,这时你的源服务器 IP 也就暴露了</p> <p>还有上面说到的 <a href="https://search.censys.io/">Censys</a>,有些人可能觉得我去上面搜自己的 IP 不就是主动暴露自己吗,确实有这个风险,但你可以去搜一下你的域名,有时候你的 DNS 解析或是服务器设置的证书中的域名也可能会在上面暴露 IP 与域名的关系</p> <h5>暴露的非标准端口</h5> <p>第二则是非标准端口的服务,其实也像上面一样,没什么安全性。大一点的扫描类服务为了数据库够大、相比同行更有竞争性,基本都会把每个 IP 能扫的端口都扫一遍,设定非标准端口并没有太大作用</p> <h5>暴露的容器端口</h5> <p>最后是容器中的端口映射,一般容器与主机的端口映射是可以修改的,很多容器自部署服务为了避免端口冲突,并不会将服务端口设为 80/443,而是 3000、5000 这种四位数的端口。为了让 IP 或域名可以不加端口直接访问,需要在服务器上设定反向代理,在收到请求时,根据传入的域名,决定转发请求到哪个容器中处理</p> <p>设置得当时,通过域名访问站点,不管源服务器上有多少个容器,反向代理都可以帮你正确的转发请求到对应的容器,而用户与 CDN 发送的请求始终都是通过标准的 80/443 HTTP/S 端口,这可以让我们无需为每个相同的 HTTP/S 服务单独设置一个端口,避免了暴露多个端口的问题</p> <p><strong>不过,你有按照我说的步骤去排查 docker 容器端口映射了吗?如果发现你的 docker 容器也可以使用 <code>IP:端口</code> 直接访问,那你就要注意一下 docker 的端口映射方式了</strong></p> <p>docker 中容器的端口映射有两种方式:暴露至公网与仅在本机暴露,下面假设容器内的端口为 2340,映射到主机的端口为 1230:</p> <pre><code class="language-bash">// 暴露至公网 // docker run 启动 docker run -p 1230:2340 &lt;容器镜像&gt; // docker compose 配置 ports: - 1230:2340 // 仅在本机暴露 // docker run 启动 docker run -p 127.0.0.1:1230:2340 &lt;容器镜像&gt; // docker compose 配置 ports: - 127.0.0.1:1230:2340 </code></pre> <p>相比之下就是在 <code>1230:2340</code> 前多加了一段 <code>127.0.0.1:</code>,可能有点难看懂,这时它实际也是分为两段,<code>127.0.0.1:1230</code> 为主机的部分,<code>2340</code> 依然为容器端口,这里我们主要是限定了这个端口只允许在 <code>127.0.0.1</code> 访问,而 <code>127.0.0.1</code> 同时也对应了 <code>localhost</code>,就仅为本机使用</p> <p>修改容器配置后,重启一下容器,再运行 <code>docker ps</code> 看看 PORTS 是不是由 <code>0.0.0.0:1230-&gt;2340/tcp</code> 变成了 <code>127.0.0.1:1230-&gt;2340/tcp</code>。这个时候,就无法通过 <code>IP:1230</code> 的方式访问到 docker 中的容器了</p> <p><strong>还有什么要注意的吗?当然,现在通过 IP 的 80/443 端口依然可以访问到服务器上的服务,跟 <a href="#暴露的-80443-标准端口">前面</a> 说的一样,只要通过 IP 访问依然能得到与通过域名访问一样的数据,那还是能猜到这两者的关联,所以我们要完全切断 IP 与域名的联系,不留任何可以被关联的数据</strong></p> <p><del>当然肯定不是让你直接关机跑路</del></p> <h4>禁止直接通过 IP 访问</h4> <p>看上去很玄乎?虽然前面也提到了 CDN 能访问到的数据,通过 IP 访问也可以,但也有方法,让 CDN 可以正常转发,而直接访问 IP 不返回任何数据</p> <p>此段部分内容来自 <a href="https://zinglix.xyz/2021/10/04/nginx-ssl-reject-handshake/">NGINX 配置避免 IP 访问时证书暴露域名 - ZingLix Blog</a> 文章,在此感谢 <a href="https://zinglix.xyz/">ZingLix</a></p> <p>假设我们使用的反向代理软件为 <a href="https://www.nginx.com/">Nginx</a>,我们原本的配置如下,其中假设自己的域名为 <code>example.com</code>:</p> <pre><code class="language-conf"># 此段作用为转发 HTTP 请求至 HTTPS server { listen 80 default_server; return 301 https://$host$request_uri; } server { listen 443 http2 ssl; server_name example.com; ssl_certificate &lt;SSL 证书公钥文件&gt;; ssl_certificate_key &lt;SSL 证书私钥文件&gt;; # 忽略了一些配置 location / { proxy_pass http://localhost:3000; # 忽略了一些配置 } } </code></pre> <p>一份很普通的 nginx 配置,除了转发 HTTP 请求到 HTTPS 外,还有一段配置,作用是收到包含 <code>example.com</code> 域名的请求时,将请求转发到本地的 <code>3000</code> 端口进行处理。解析好 DNS 或同时加上 CDN,你都可以通过 <code>example.com</code> 域名来访问到服务器上的 <code>3000</code> 端口对应的服务</p> <p>不过,如果你通过浏览器直接访问 IP,不管使用 <code>http://</code> 还是 <code>https://</code> 前缀,最后都是访问到了 443 端口,因为第一端的转发请求起了作用。而下一步就是请求 443 端口了,第二段配置的 <code>listen 443</code> 意思就是监听 443 端口,那么这个请求自然就传到第二段配置设定的 <code>3000</code> 端口对应的服务里去了,这就造成了使用 IP 能直接访问到服务器上的服务,我们可不希望这样</p> <h5>添加配置拒绝直接通过 IP 访问时的请求</h5> <p>那怎么办?我们可以在第一段后面添加一段监听 <code>443</code> 端口的配置,返回 403 或者 404?你可以现在试一下:</p> <pre><code class="language-conf"># 此处作用为转发 HTTP 请求至 HTTPS server { listen 80 default_server; return 301 https://$host$request_uri; } server { listen 443 ssl default_server; return 403; } # 忽略了第三段配置 </code></pre> <p>然后你会不出所料的得到类似 <code>no &quot;ssl_certificate&quot; is defined for the &quot;listen ... ssl&quot;</code> 的错误,简单来说就是,想要监听 443 端口,你必须要设定一个 SSL 证书,当然这个证书是否有效并不重要,只要是个证书就可以</p> <p>但在添加证书前,我们先看看证书里有哪些内容(来自 <a href="https://www.cloudflare.com/zh-cn/learning/ssl/what-is-an-ssl-certificate/">什么是 SSL 证书? | Cloudflare</a>):</p> <pre><code class="language-text">针对其颁发证书的域名 证书颁发给哪一个人、组织或设备 证书由哪一证书颁发机构颁发 证书颁发机构的数字签名 关联的子域 证书的颁发日期 证书的到期日期 公钥(私钥为保密状态) </code></pre> <p>看到了吗,第一行中就有关于域名的信息,这可不行,在这个配置里加 SSL 证书也会泄露我们的域名,怎么办呢?</p> <p>也有办法,只要我们给一个不包含我们域名信息的 SSL 证书就可以了,毕竟 nginx 并不会检验证书的有效性,于是我们使用 openssl 生成一个私钥和证书:</p> <pre><code class="language-bash"># 请在 Linux 下运行,生成后的证书将生成在运行代码时的目录 # 在运行第二行时,会询问你证书的信息,可以随便填写也可以直接全部回车 openssl genpkey -algorithm RSA -out private.key openssl req -new -key private.key -out cert.csr openssl x509 -req -in cert.csr -signkey private.key -out certificate.crt </code></pre> <p>运行以上命令后,目录下会多出来三个文件,分别是 <code>private.key</code>、<code>cert.csr</code> 和 <code>certificate.crt</code>,将其添加到前面的配置里:</p> <pre><code class="language-conf"># 忽略了第一段配置 server { listen 443 ssl default_server; ssl_certificate &lt;文件目录&gt;/certificate.crt; ssl_certificate_key &lt;文件目录&gt;/private.key; return 403; } # 忽略了第三段配置 </code></pre> <p>此时再去直接访问 IP,你就会得到证书无效的错误,当然你也可以在页面上找一找,一般会有高级选项允许继续访问,就会得到 403 或 404 的错误,看你是如何设置的</p> <p>不过我们是为了防止证书暴露信息,所以这是无法访问也是在我们的预期中的。点击浏览器地址栏旁边的信息按钮,这个按钮一般会显示成一个小锁或是类似设置的图标,就能看到证书无效的提示,点击查看详细信息就在弹出的窗口里可以看到里面的证书信息,这些信息就是你在前面生成证书时填写的信息</p> <p>到这里,已经很难将你的源站与域名联系起来了,只要你使用 openssl 生成的证书中没有明晃晃写上你的域名,那想通过特征找到源站对应的域名,就等于大海捞针了</p> <p><strong>如果你的 Nginx 版本高于 1.19.4,你还可以使用新功能:拒绝握手</strong></p> <h5>设定直接通过 IP 访问时拒绝握手</h5> <p><strong>注意:这个功能和上一个方法作用类似,你只需要选择其中的一个来设置。相对而言,拒绝握手能提供更少的信息,效果更佳</strong></p> <p>这个功能需要 Nginx 版本高于 1.19.4,你可以在终端运行一下 <code>nginx -v</code> 检查一下是否可以使用该新功能。当然你也可以去升级 Nginx,如果无法升级或其他原因不想升级,上面的方法也足够安全了</p> <p>拒绝握手的方法配置起来很简单,与上面的方法类似,只需在正常配置前添加一个监听 443 端口的服务即可,可以不指定 SSL 证书:</p> <pre><code class="language-conf"># 此处作用为转发 HTTP 请求至 HTTPS server { listen 80 default_server; return 301 https://$host$request_uri; } server { listen 443 ssl default_server; # 当配置中开启拒绝握手时,不需要添加 SSL 证书与私钥 ssl_reject_handshake on; } # 忽略了第三段配置 </code></pre> <p>保存配置并重启 nginx 服务后,再去浏览器里访问 IP,就可以看到已经无法通过 IP 访问到站点了,而且页面信息里也没有证书信息,可谓非常的干净</p> <p>第二段配置中的 <code>ssl_reject_handshake on</code> 就是关键所在,只要一段配置中启用了这个功能,那么走这个通道的请求都会被拒绝握手,不会提供任何信息,所以注意不要往正常的服务配置里启用这个功能</p> <h4>我们安全了?暂时的</h4> <p>直到这里,我们开启了 CDN、关闭了暴露的端口、禁止 IP 访问以及配置了证书避免泄露我们的域名,不过还有一个方式能访问到服务,那就是我们的域名</p> <p>为什么配置了怎么多,IP 都不能访问到网站了,而通过域名还可以?下面有两行命令,你可以试一试:</p> <pre><code class="language-bash">curl -v -k https://&lt;你的源站 IP&gt; curl -v -k https://&lt;你的源站所绑定的域名&gt; </code></pre> <p>第一行是直接通过 IP 请求内容,第二行相同,但走的是域名来请求内容,可能还会经过 CDN。看着我们是配置成功了,IP 会无法访问,域名访问一切正常,不过,如果你试试下面这行命令呢?</p> <pre><code class="language-bash">curl -v -k https://&lt;你的源站所绑定的域名&gt; --resolve &lt;你的源站所绑定的域名&gt;:443:&lt;你的源站 IP&gt; </code></pre> <p>看似能正常从域名获取网页内容?没错,不过我可要告诉你,这里请求的其实是 IP,而不是你的域名,是不会经过 CDN 的。但我们配置了禁止 IP 访问,为什么还是能获取到站点内容呢?</p> <h5>依然有办法可以通过 IP 访问</h5> <p>先看看这行命令有哪些参数,下面的域名将被替换为 example.com,IP 被替换为 333.333.333.333,CDN 的 IP 被替换为 444.444.444.444:</p> <pre><code class="language-bash">curl \ -v \ # 输出请求时的整个过程 -k \ # 请求时跳过 SSL 检测(不会因为证书原因导致请求失败) https://example.com \ # 要请求的域名 --resolve \ # 将某个域名强制解析到某个 IP(替代 DNS 的作用) example.com:443:333.333.333.333 # 要强制指定的数据 </code></pre> <p>这么看,这行命令的作用就是:在请求时输出全部日志并跳过 SSL 检测,跳过正常的 DNS 环节,直接将域名解析到指定的 IP 地址,再向强制解析后的域名发送请求,运行后的输出类似这样:</p> <pre><code class="language-bash">$ curl -v -k https://example.com --resolve example.com:443:333.333.333.333 * Added example.com:443:333.333.333.333 to DNS cache * Hostname example.com was found in DNS cache * Trying 333.333.333.333:443... * Connected to example.com (333.333.333.333) port 443 (#0) # 忽略后续信息 </code></pre> <p>而正常通过域名访问的输出呢?</p> <pre><code class="language-bash">$ curl -v -k https://example.com * Trying 444.444.444.444:443... * Connected to example.com (444.444.444.444) port 443 (#0) # 忽略后续信息 </code></pre> <p>可以看到,如果指定了 <code>--resolve example.com:443:333.333.333.333</code> 参数,在请求时会将 <code>example.com</code> 直接解析到 <code>333.333.333.333</code>,之后请求会直接冲着源服务器去,服务器再返回站点数据,我们的服务器可能就会再次沦陷了</p> <p>怎么办呢?说实话,如果攻击者到了这种拿着域名一个个试试 IP 能不能通过请求的时候,我们就只能限制哪些 IP 可以直接访问服务器了</p> <h5>限制哪些 IP 可以直接访问源服务器</h5> <p>想要限制哪些 IP 可以访问我们的源服务器,我们总不能手动将用户的 IP 地址添加到防火墙白名单,家庭宽带很多时候并不会有固定 IP,每次用户 IP 变动时,你都需要实时将用户的 IP 添加到防火墙白名单,这相当的麻烦,而且你总不能在用户的设备上安装软件实时检测 IP 变化,这个解决方法很不现实</p> <p>但如果我们使用的是 CDN,那解决方法就很方便了,由于 CDN 会负责转发用户的请求,使用 CDN 后用户与源服务器的请求始终是由 CDN 负责的,服务器日志中只会有来自 CDN IP 的请求,我们只需要使用防火墙拒绝其他 IP 的连接请求,仅允许来自 CDN IP 的请求即可</p> <p>不过,你需要提前查询一下你使用的 CDN 供应商是否能提供 CDN 全部 IP 的列表,不然下方的两个方法都不适用你的情况</p> <p>下面部分内容来自 <a href="https://zikin.org/block-censys-scanner/#header-id-4">屏蔽 Censys 扫描器, 及设置仅允许 Cloudflare 回源 - Zikin 的独立博客</a> 文章,同样在此感谢 <a href="https://zikin.org/">Zikin</a></p> <p>我们有两种方式可以对设置仅允许哪些 IP 与源服务器建立连接:</p> <p><strong>方法 1: 使用 Nginx 的 <code>allow</code> / <code>deny</code> 字段 (不推荐)</strong></p> <blockquote> <p>由于这是一个不被推荐的方法,我并不会写出具体的操作流程,如果你还是想看看如何配置,你可以去 <a href="https://zikin.org/block-censys-scanner/#header-id-4">屏蔽 Censys 扫描器, 及设置仅允许 Cloudflare 回源 - Zikin 的独立博客</a> 文章查看操作步骤</p> </blockquote> <p>这个方法是可以不向被限制的 IP 发送原本的网页数据,但在搭配了上面添加的 nginx 拒绝握手特性时,会出现一个自欺欺骗人的情况。我们先保留之前的拒绝握手设置,再往普通配置里添加 <code>deny</code> 字段拒绝所有 IP 的连接请求:</p> <pre><code class="language-conf"># 此处作用为转发 HTTP 请求至 HTTPS server { listen 80 default_server; return 301 https://$host$request_uri; } server { listen 443 ssl default_server; ssl_reject_handshake on; } server { listen 443 http2 ssl; server_name example.com; ssl_certificate &lt;SSL 证书公钥文件&gt;; ssl_certificate_key &lt;SSL 证书私钥文件&gt;; # 忽略了一些配置 # 方便测试,这里我们将拒绝来自所有 IP 的连接请求,使用时,你需要改为仅允许来自你 CDN 供应商的 IP deny all; location / { proxy_pass http://localhost:3000; # 忽略了一些配置 } } </code></pre> <p>我们再使用 curl 发送请求,域名被替换为 example.com,源站 IP 依然为 333.333.333.333:</p> <p>我们先试试直接请求 IP</p> <pre><code class="language-bash">$ curl -v -k https://333.333.333.333 * Trying 333.333.333.333:443... * Connected to 333.333.333.333 (333.333.333.333) port 443 (#0) * ALPN: offers h2,http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS alert, unrecognized name (624): * OpenSSL: error:0A000458:SSL routines::tlsv1 unrecognized name * Closing connection 0 curl: (35) OpenSSL: error:0A000458:SSL routines::tlsv1 unrecognized name </code></pre> <p>与预期相同,服务器拒绝了我们发送的握手请求,也没有提供证书,接下来我们试试强制将域名解析到源站的 IP 地址:</p> <pre><code class="language-bash">$ curl -v -k https://example.com --resolve example.com:443:333.333.333.333 * Added example.com:443:333.333.333.333 to DNS cache * Hostname example.com was found in DNS cache * Trying 333.333.333.333:443... * Connected to example.com (333.333.333.333) port 443 (#0) * ALPN: offers h2,http/1.1 # 忽略了部分 TLS 握手内容 * ALPN: server accepted h2 * Server certificate: # 忽略了服务器证书内容 # 忽略了请求信息 &gt; GET / HTTP/2 &gt; Host: example.com &gt; user-agent: curl &gt; accept: */* &gt; * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): * old SSL session ID is stale, removing &lt; HTTP/2 403 &lt; server: nginx &lt; content-type: text/html &lt; content-length: 153 &lt; &lt;html&gt; &lt;head&gt;&lt;title&gt;403 Forbidden&lt;/title&gt;&lt;/head&gt; &lt;body&gt; &lt;center&gt;&lt;h1&gt;403 Forbidden&lt;/h1&gt;&lt;/center&gt; &lt;hr&gt;&lt;center&gt;nginx&lt;/center&gt; &lt;/body&gt; &lt;/html&gt; * Connection #0 to host example.com left intact </code></pre> <p>可以看到,curl 成功与源服务器握手,并输出了来自 nginx 的回应,虽然最后输出的是 403 Forbidden(无权访问) 的回应,也没有输出原本的网页信息。看上去 IP 白名单起效果了,但它并没有达到我们想要的效果,此时我们再试试将一个不存在的域名解析到源站的 IP 地址:</p> <pre><code class="language-bash">$ curl -v -k https://noexistdomian --resolve noexistdomian:443:333.333.333.333 * Added noexistdomian:443:333.333.333.333 to DNS cache * Hostname noexistdomian was found in DNS cache * Trying 333.333.333.333:443... * Connected to noexistdomian (333.333.333.333) port 443 (#0) * ALPN: offers h2,http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS alert, unrecognized name (624): * OpenSSL: error:0A000458:SSL routines::tlsv1 unrecognized name * Closing connection 0 curl: (35) OpenSSL: error:0A000458:SSL routines::tlsv1 unrecognized name </code></pre> <p>从最后的输出可以看出,这个不存在的域名,触发了我们的拒绝握手配置</p> <p><strong>如果向源服务器发送请求时传递的域名正确,则可成功握手,否则将返回拒绝握手的 <code>tlsv1 unrecognized name</code> 错误</strong></p> <p>这代表当攻击者使用域名对我们的服务器进行扫描时,直接请求 IP 或附加的是错误的域名,那么你的服务器会正常的拒绝握手。但如果请求的是正确的域名,服务器接收了请求、完成了握手,然后返回了 403 Forbidden(无权访问)</p> <p>有什么问题呢?将视角换为攻击者就好理解了:我直接请求 IP 你拒绝我的握手,用其他域名请求你也拒绝握手,但我用 <code>example.com</code> 你通过了握手再给我返回 403?那这个 IP 绝对跟 <code>example.com</code> 这个域名有关系</p> <p>后面的就不用我说了吧,可以等着换下一个 IP 了</p> <p><strong>方法 2: 使用系统防火墙</strong></p> <p>在这里,我们使用基于 <code>iptables</code> 易用接口的 <code>ufw</code> 设定防火墙规则,此处部分内容同样来自 <a href="https://zikin.org/block-censys-scanner/#header-id-4">屏蔽 Censys 扫描器, 及设置仅允许 Cloudflare 回源 - Zikin 的独立博客</a> 文章</p> <p>我们照样先试试被防火墙封禁后,直接请求 IP 与携带域名请求会怎么样,先设置防火墙:</p> <pre><code class="language-bash">sudo ufw allow OpenSSH # 设定防火墙允许 ssh,否则可能就连不上服务器了 sudo ufw default deny # 将默认入站连接设定为拒绝 </code></pre> <p><strong>注意!如果你需要通过 ssh 才能连接到服务器,请务必按照上面的方法允许 ssh 请求,否则你可能会再也无法连接到你的服务器</strong></p> <p>此时,我们已经设定好了防火墙配置,允许 ssh 连接,并拒绝所有入站请求,接下来我们就可以通过 ufw 开启防火墙了:</p> <pre><code class="language-bash">sudo ufw enable # 启用防火墙 sudo ufw status verbose # 查看防火墙状态以及规则 # 此时的输出应如下所示 Status: active Logging: on (low) Default: deny (incoming), allow (outgoing), deny (routed) New profiles: skip To Action From -- ------ ---- 22/tcp (OpenSSH) ALLOW IN Anywhere 22/tcp (OpenSSH (v6)) ALLOW IN Anywhere (v6) </code></pre> <p>这时候再去试试请求服务器,你会发现不管是单独请求 IP 还是携带域名请求都无法获取到内容了,可能会提示无法连接或者迟迟没有完成请求,因为这个时候请求到达防火墙的时候直接被拒绝了,没有返回任何信息</p> <p>不过这个时候再去浏览器访问域名,你会发现域名也进不去了,因为上面的规则阻止了所有 IP 的入站请求,CDN 的 IP 也同样被阻止了,所以我们还需要设定规则允许来自 CDN IP 的连接</p> <p>与用户多变的 IP 不同,CDN 的 IP 一般是固定的,我们只需要将 CDN 供应商提供的 CDN IP 列表添加到防火墙白名单即可</p> <p>我使用的 CDN 来自 Cloudflare,如果你使用的是其他 CDN 提供商,请手动查找对应的 IP 范围列表,按照 <a href="#限制哪些-ip-可以直接访问源服务器">前面</a> 的需求,Cloudflare 是提供全部 IP 的列表的,可见 <a href="https://www.cloudflare.com/ips/">IP 范围 | Cloudflare</a>,我们只需要将列出的 IP 范围添加到防火墙白名单,通过域名访问的请求即可恢复正常:</p> <pre><code class="language-bash">sudo ufw allow from 173.245.48.0/20 sudo ufw allow from 103.21.244.0/22 # 省略后面一条条加的过程 </code></pre> <p>测试的时候我觉得很麻烦,于是我写了一个 shell 脚本,发布在 <a href="https://gist.github.com/Interstellar750/1803cdefcaa91940e87a3d27fe78f17b">GitHub Gists</a> 上,可以一键获取 Cloudflare CDN 的所有 IP 范围列表并添加到防火墙白名单,你可以使用它来一键添加:</p> <pre><code class="language-bash">curl -L https://gist.githubusercontent.com/Interstellar750/1803cdefcaa91940e87a3d27fe78f17b/raw/add_cf_ips.sh | sudo bash # 如果你想从防火墙白名单中移除 Cloudflare 的 IP,执行下面这行 # 运行后会在运行时的目录留下来一个 add_cf_ips.sh 文件,你可能需要手动删除它 curl -L https://gist.githubusercontent.com/Interstellar750/1803cdefcaa91940e87a3d27fe78f17b/raw/add_cf_ips.sh &gt; add_cf_ips.sh &amp;&amp; sudo bash add_cf_ips.sh --remove </code></pre> <p>对了,在添加完防火墙规则后,你可能需要运行一下 <code>sudo ufw reload</code> 加载修改后的规则以生效,之后再去浏览器使用域名访问,就可以正常访问了。使用 curl 请求 IP 或携带域名请求 IP,都不会返回站点信息,我们就完成了对源服务器的保护</p> <h3>最后的一些琐事</h3> <p>如果设定了防火墙白名单,那么上面 nginx 的拒绝握手配置其实也可有可无了,非 CDN 的 IP 根本无法触发到这个拒绝握手的规则,最坏的情况只能是攻击者用 CDN 的 IP 去扫服务器请求,如果真有这种情况,可以考虑换一个 CDN 提供商了</p> <p>在想到防火墙添加 Cloudflare CDN 的 IP 白名单时,我还在考虑如果攻击者连上 warp 之后扫服务器怎么办,我就去查了查 warp 的 IP 段,跟 CDN 并不在一个段。这下除非去机房一台机一台机的拔网线,不然基本就不可能找得到我 IP 了</p> <p>文章末尾感谢:</p> <p><strong><a href="https://www.cloudflare.com/">Cloudflare</a></strong> <br> DNS / CDN / PaaS 提供商,提供了很多好用的服务,帮我缓解了很多通过 CDN 的攻击,让我的网站不至于关站跑路</p> <p><strong><a href="https://vercel.com/">Vercel</a></strong> <br> PaaS 提供商,服务真的非常好用,尽管我的流量被攻击刷到远超 100GB 的 1.2TB 后,没有向我发送账单也没有封禁我的账号</p> <p><strong>本文提到文章的两位作者</strong> <br> 再次感谢您的教程,让我可以学习到需要的网络安全知识</p> <p><strong>某个服务器提供商</strong> <br> 由于安全原因我这里不能说是哪个服务器提供商,但还是在此感谢一下,不然我就没地方放自部署服务了</p> <p><strong>不知名的攻击者</strong> <br> 从 23 年 7 月开始将我服务器打到空路由,在我建立防护后顶着 Cloudflare 一直给我刷了估计有 30m 请求。同时给我 Vercel 的免费 100 GB 套餐超额刷至 1.2TB,让我被迫学习网络安全知识</p>

2024/4/5
阅读更多

域名变动

<p>个人站点的域名将迎来一些变动,位于 <code>trle5.dev</code> 域名下的全部站点,其绑定的域名将被迁移到 <code>trle5.xyz</code> 域名下</p> <p><strong>站点迁移后使用的域名:</strong></p> <table> <thead> <tr> <th align="left">服务</th> <th align="left">先前域名</th> <th align="left">新域名</th> <th align="left">备注</th> </tr> </thead> <tbody> <tr> <td align="left"><strong>Hubert's Gitea</strong></td> <td align="left"><a href="https://trle5.dev/">trle5.dev</a></td> <td align="left"><a href="https://gitea.trle5.xyz/">gitea.trle5.xyz</a></td> <td align="left"></td> </tr> <tr> <td align="left"><strong>Hubert's Alist</strong></td> <td align="left"><a href="https://alist.trle5.dev/">alist.trle5.dev</a></td> <td align="left"><a href="https://alist.trle5.xyz/">alist.trle5.xyz</a></td> <td align="left"></td> </tr> <tr> <td align="left"><strong>Cloudflare tunnle</strong></td> <td align="left"><a href="https://git.trle5.dev/">git.trle5.dev</a></td> <td align="left"><a href="https://git.trle5.xyz/">git.trle5.xyz</a></td> <td align="left"></td> </tr> </tbody> </table> <h2>日程</h2> <p>从此文章发布起,当您访问任何一个使用 <code>trle5.dev</code> 域名或子域名的站点时,您都能在网页顶部看到一个提示,并有一个超链接将您引导到这个页面</p> <p>在 4 月 1 日前,您依然可以照常通过旧域名访问 <code>trle5.dev</code> 下的全部站点,但我不保证功能是否依然正常</p> <p>4 月 1 日后,位于 <code>trle5.dev</code> 域名及其子域名的站点将被 301 重定向至新域名,直到 6 月 4 日域名到期 DNS 停止解析</p> <p><code>trle5.dev</code> 域名过期后,可能会购入重新使用,如果域名被重新注册,您应该能在网页顶部再此看到一个链接到此页面的提示,文中应该也更新了域名的使用信息</p>

2024/2/16
阅读更多

为旧 iOS 设备下载适用的旧版软件

<p>需要的东西:</p> <ul> <li>任意区 iCloud 账号</li> <li>能安装最新版软件的 iOS 设备</li> </ul> <blockquote> <p>上一条需求听上去有点扯,都有另一个设备能装最新版了那还为什么装旧版软件。这另一个设备主要作只用是入库,即让软件出现在应用商店的“已购项目”中,如果实在没有也可以找朋友帮忙</p> </blockquote> <ol> <li>在旧设备和新设备的应用商店中登录同一个 iCloud 账号</li> <li>在新设备中搜索想在旧设备上安装的软件,安装对应软件,只要过了验证后,出现应用下载进度条,就可以取消了</li> <li>打开旧设备的应用商店,找到“已购项目”页面,不出意料你能在里面找到刚刚的那个软件,点击安装,会提示“该软件不兼容此 iOS,是否下载兼容的最后一个版本”,选择“是”即可</li> </ol> <h3>注意事项</h3> <p>此方法只能下载软件兼容旧版 iOS 系统的最新一个版本</p> <p>如果你要下载的软件从发布起就要求很高的 iOS 版本(例如 ChatGPT 最低要求 iOS 16.1),这种情况应用商店根本没有符合旧系统的旧版应用下载,那也是没办法的</p> <p>如果 iOS 系统版本高于或等于目前软件适配的最高系统版本,就没法通过此方法安装旧版软件</p> <p>例如 iOS 12 在今年发布,目前某个软件的系统要求如下</p> <pre><code>1.0 版本需要 iOS 9+ 2.0 版本需要 iOS 10+ 3.0 版本需要 iOS 11+ 3.5 版本需要 iOS 11+ 4.0 版本需要 iOS 12+ </code></pre> <p>现在软件版本号已经是 4.0,最低要求 iOS 12</p> <p>使用 iOS 9 的设备去安装该软件时,应用商店会询问你是否安装兼容该系统的的最后一个版本,选择是后,应用商店会给你安装 1.0 版本的软件。同理,使用 iOS 10 的设备会安装 2.0 版本</p> <p>但使用 iOS 11 来安装时,虽然 3.0 和 3.5 都兼容 iOS 11,但应用商店只会安装 3.5 版本,因为这是兼容 iOS 11 的最新一个版本,想安装 3.0 版本就只能找其他办法了</p> <p>如果使用 iOS 12+ 的设备来安装这个软件,不管你是搜索直接安装,还是在“已购项目”中安装,下载到的永远都是最新的 4.0 版本</p> <p>下载后的旧版软件能正常安装不代表能正常使用,因为服务器可能会对旧版本进行禁止登录或其他限制使用的行为,这样就没有其他办法了</p>

2024/1/9
阅读更多

隐私声明

<p>本站源代码存放在 <a href="https://github.com/interstellar750/hexo_s/">GitHub</a> 仓库,博客托管在 <a href="https://pages.cloudflare.com/">Cloudflare Pages</a> 平台,域名 DNS 解析由 <a href="https://www.cloudflare.com/">Cloudflare</a> 提供,我可得知的数据如下:</p> <h2>GitHub 仓库</h2> <ul> <li> <p>stars / watching / fork 的用户列表</p> </li> <li> <p>Discussions 的讨论内容(包括 Giscus)</p> </li> <li> <p>Pull Request 请求信息</p> </li> </ul> <h2>Cloudflare / Cloudflare Pages 平台</h2> <p><a href="https://developers.cloudflare.com/analytics/account-and-zone-analytics/zone-analytics/#http-traffic">流量分析</a></p> <p>Cloudflare 为域名托管者提供了免费的流量分析,最长可以记录 30 天的数据,借此,我可以得知的数据如下:</p> <blockquote> <p>此功能为使用 Cloudflare 域名托管服务时默认开启的功能,您可能无法通过任何方式绕过这部分的数据收集</p> </blockquote> <details no-indent> <summary><b>Web 流量</b></summary> <ul> <li>请求(最长 30 天,最短 24 小时,精确到请求数) <ul> <li>请求总数</li> <li>已缓存的请求</li> <li>未缓存的请求</li> </ul> </li> <li>带宽(最长 30 天,最短 24 小时,精确到 KB) <ul> <li>总带宽</li> <li>已缓存的带宽</li> <li>未缓存的带宽</li> </ul> </li> <li>唯一访问者(精确到人数) <ul> <li>总计(最长 30 天,最短 24 小时)</li> <li>上限(最长 1 天,最短 1 小时)</li> <li>下限(最长 1 天,最短 1 小时)</li> </ul> </li> </ul> </details> <details no-indent> <summary><b>Web 流量请求</b></summary> <ul> <li>访客流量靠前的国家或地区(精确到流量数,最长 30 天,最短 24 小时)</li> <li>统计信息(上个月) <ul> <li>已节省的字节数(精确到 KB)</li> <li>已提供的 SSL 请求数(精确到次数)</li> <li>已阻止的攻击数(精确到次数)</li> </ul> </li> </ul> </details> <p><a href="https://developers.cloudflare.com/web-analytics/">Web Analytics</a></p> <p>Cloudflare Web Analytics 是一个基于 JavaScript 代码的分析工具,最长可以记录 180 天的数据</p> <blockquote> <p>您可以尝试使用一些工具来屏蔽 <code>https://static.cloudflareinsights.com/beacon.min.js</code> JavaScript 代码来绕过此部分的数据收集。您也可以选择对于本站点(trle5.xyz)屏蔽 JavaScript 代码,但这可能会导致一些功能将无法使用</p> </blockquote> <p>在控制台中,可以按最长 30 天,最短 1 分钟的时间间隔来查看数据,同时还可以使用 <code>国家/地区</code> <code>主机</code> <code>路径</code> <code>引用方</code> <code>设备类型</code> <code>浏览器</code> <code>操作系统</code> <code>站点</code> <code>排除自动程序</code> 来作为筛选条件,借此,我可以得知的数据如下:</p> <details no-indent> <summary><b>高级指标</b></summary> <ul> <li>页面加载时间(精确到毫秒)</li> <li>访问量(精确到次数)</li> <li>页面浏览量(精确到次数)</li> <li>核心网络指标(Core Web Vitals)见下方</li> </ul> </details> <details> <summary><b>核心网络指标(Core Web Vitals)</b></summary> <ul> <li>最大内容绘制(LCP)(精确到次数)</li> <li>与下一个画图交互(INP)(精确到次数)</li> <li>累计布局偏移(CLS)(精确到次数)</li> </ul> <p>以上三个指标数据,均可按照 <code>URL</code> <code>浏览器</code> <code>操作系统</code> <code>国家/地区</code> <code>元素</code> 类别来排序</p> <p>在控制台中还提供了调式视图,在调式视图中可以查看被记为 <strong>差</strong> 和 <strong>需要改进</strong> 元素的详情信息,其中也可查看到一些数据:</p> <details no-indent> <summary><b>最大内容绘制(LCP)</b></summary> <ul> <li>Object 和 页面 <ul> <li>主机</li> <li>路径</li> </ul> </li> <li>指标延迟 <ul> <li>LCP - P50</li> <li>LCP - P75</li> <li>LCP - P90</li> <li>LCP - P99</li> </ul> </li> <li>DOM <ul> <li>元素</li> </ul> </li> </ul> </details> <details no-indent> <summary><b>与下一个画图交互(INP)</b></summary> <ul> <li>页面 <ul> <li>主机</li> <li>路径</li> </ul> </li> <li>指标 <ul> <li>INP - P50</li> <li>INP - P75</li> <li>INP - P90</li> <li>INP - P99</li> </ul> </li> <li>DOM <ul> <li>元素</li> </ul> </li> </ul> </details> <details no-indent> <summary><b>累计布局偏移(CLS)</b></summary> <ul> <li>页面 <ul> <li>主机</li> <li>路径</li> </ul> </li> <li>指标 <ul> <li>CLS - P50</li> <li>CLS - P75</li> <li>CLS - P90</li> <li>CLS - P99</li> </ul> </li> <li>DOM <ul> <li>元素</li> </ul> </li> <li>布局偏移 <ul> <li>上一个</li> <li>当前</li> </ul> </li> </ul> </details> </details> <h2>关于统计数据</h2> <p>统计数据中绝大部分可展示的数据已列举于上方,但此列表可能依然会有遗漏,若有其他需要了解的数据请前往对应平台中查看相应文档</p> <details no-indent> <summary><b>您在本站的浏览统计数据可能会被公开展示,点此查看公开展示时的类似形式</b></summary> <ul> <li>与他人展示 <ul> <li>你看看我这篇文章竟然有这么多人看:(附上页面浏览数据)</li> </ul> </li> <li>演示流量消耗 <ul> <li>压缩了图片大小后,我博客每个月只需要不到 1GB 流量:(附上流量消耗统计)</li> </ul> </li> <li>浏览器类型 <ul> <li>访问我博客用的浏览器七八成都是 Chrome:(附上浏览器类型统计)</li> </ul> </li> </ul> </details>

2025/10/24
阅读更多

待办事项

有时候也得记一下自己要干嘛

2026/3/8
阅读更多

建站历程

因为提前感觉到这个页面会变得很长很长,就先分离出来了

2025/10/24
阅读更多

关于我

<p><strong>大家好啊,我是 Hubert,<del>今天来点大家想看的东西啊</del></strong></p> <h3>会些什么</h3> <p><strong>Go 语言</strong> 现在写了几个小项目,自我认为还可以</p> <p><strong>C 语言</strong> 说实话已经很久没写过了</p> <p><strong>Linux</strong> 没什么变化,会些日常用的命令就足够了</p> <h3>兴趣爱好</h3> <p>日常消遣方式是在 Telegram 上聊天与刷频道,其次就是玩游戏</p> <p>有时会画点画画,但还没到入门的阶段</p> <h3>联系方式</h3> <p>依然是没有国内的交流平台,不是很喜欢用,有联系需求就发邮件吧</p> <p><strong>Telegram:</strong> <a href="https://t.me/trle5">@trle5</a> / <a href="https://t.me/include011">@include011</a></p> <p><strong>Matrix:</strong> <a href="https://matrix.to/#/@trle5:matrix.org">@trle5:matrix.org</a> 不怎么活跃,但应该能收到消息</p> <p><strong>Email:</strong> <a href="mailto:01@trle5.xyz">01@trle5.xyz</a> 用的 Cloudflare 转发到 Gmail</p> <p><strong>Twitter:</strong> <a href="https://twitter.com/interstellar750">@interstellar750</a> 不是很喜欢发推文</p> <p><strong>Fediverse:</strong> <a href="https://social.hyp3r.link/@trle5">trle5@hyp3r.link</a> 在 <a href="https://social.hyp3r.link/@kwa">kwaa</a> 大佬的实例上注册的,也是不怎么喜欢发文</p> <p><strong>GitHub:</strong> <a href="https://github.com/interstellar750">@interstellar750</a> (好像并不能联系到人...)</p> <p><strong>Gitea:</strong> <a href="https://gitea.trle5.xyz/trle5">@trle5</a> 自己在 VPS 上用 docker 跑了个 Gitea,已经运行三年了</p> <h4>GPG 公钥</h4> <details no-indent> <summary><big><code>AAC3 7641 0634 FF86</code></big></summary> <pre><code class="language-bash">sec rsa4096/AAC376410634FF86 2023-01-01 [SC] (签名 证明) 7456 A0AB 47EC E8BE 1AD0 89D9 AAC3 7641 0634 FF86 uid [ultimate] Hubert Chen &lt;01@trle5.xyz&gt; ssb rsa4096/B716CE1EAA7B8F00 2023-01-01 [E] (加密) ssb rsa4096/B4ED58260C725C91 2023-01-06 [A] (认证) ssb rsa4096/2935B4DE0D6F7720 2023-01-06 [SE] (签名 加密) </code></pre> </details> <details no-indent> <summary><big><code>8A2A 227E 222D CCDB</code></big></summary> <pre><code class="language-bash">sec# ed25519/8A2A227E222DCCDB 2023-01-18 [C] (证明) F154 5A09 2296 673A 0C43 6BE0 8A2A 227E 222D CCDB uid [ultimate] Hubert Chen &lt;01@trle5.xyz&gt; ssb&gt; ed25519/74D8BCE883FDDEE2 2023-01-19 [S] (签名) ssb&gt; cv25519/FA47AF4129AA0BB1 2023-01-19 [E] (加密) ssb&gt; ed25519/7043720D3C7D7718 2023-01-19 [A] (认证) </code></pre> </details> <blockquote> <p>当你需要确认屏幕前的人为“我”时,你不需要任何理由即可向我索要以上方 GPG 密钥进行签名的消息,收到签名的消息时请务必验证消息的时效性,同时请确保验证消息中包含双方的信息以及需要的操作内容</p> <p>若验证失败或以任何方式拒绝或推脱签名请求,请不要信任此时“我”的任何请求!</p> </blockquote> <p>获取公钥文件: <a href="https://github.com/Interstellar750.gpg">GitHub</a> | <a href="https://gitea.trle5.xyz/trle5.gpg">Hubert's Gitea</a> | <a href="https://t5d.trle5.xyz/GPG/gpg_pub_keys_hubert.asc">Hubert's Box</a></p> <p>目前主要用来给 Commit 签名的密钥为 <code>2935 B4DE 0D6F 7720</code>,暂未对其他 GPG 公钥进行证明</p> <h4>游戏平台</h4> <p><strong>Steam:</strong> <a href="https://steamcommunity.com/id/interstellar750">interstellar</a> <br> <strong>Xbox:</strong> <a href="https://account.xbox.com/zh-cn/profile?gamertag=interstellar771">interstellar771</a> <br> <strong>Ubisoft:</strong> <a href="https://ubisoftconnect.com/zh-CN/profile/interstellar750">interstellar750</a> <br> <strong>Minecraft ID:</strong> <a href="https://namemc.com/profile/trle5">trle5</a> <br> <strong>EA ID:</strong> <code>trle5</code> <br></p> <h4>音乐</h4> <p><strong>Spotify:</strong> <a href="https://open.spotify.com/user/31fuag5tpkvedxdfbkbt5zrygfgq">Hubert Chen</a> <br> <strong>网易云音乐:</strong> <a href="https://music.163.com/#/user/home?id=1884310694">trle5</a> 不是很常用 <br></p> <p>目前主要是用 Spotify,喜欢听的音乐类型比较杂</p> <h3>设备</h3> <h4>移动设备</h4> <p><a href="https://zh.m.wikipedia.org/wiki/IPhone_12">iPhone 12</a> A14 / 4GB / 128GB <br> <a href="https://zh.m.wikipedia.org/zh-hans/IPhone_SE_(第二代)">iPhone SE 2</a> A13 / 3GB / 64GB 坏掉了... <br> <a href="https://zh.m.wikipedia.org/zh-hans/IPad_mini_(第五代)">iPad mini 5</a> A12 / 3GB / 64GB <br> <a href="https://zh.m.wikipedia.org/zh-hans/Apple_Watch_Series_5">Apple Watch S5</a> 44mm / 32GB <br></p> <h4>硬件</h4> <p><a href="https://www.intel.com/content/www/us/en/products/sku/80817/intel-core-i54460-processor-6m-cache-up-to-3-40-ghz/specifications.html">i5-4460 / HD 4600</a> &amp; <a href="https://www.nvidia.cn/content/dam/en-zz/zh_cn/Solutions/deep-learning/deep-learning-solutions/inference-platform/hpc/222754_Tesla_P4_Datasheet_nvidia-hr-cn.pdf">Tesla P4</a> / 8GB DDR3 / 240GB SATA SSD x 3 <br> <a href="http://www.orangepi.cn/html/hardWare/computerAndMicrocontrollers/service-and-support/Orange-Pi-Zero-2.html">Orange Pi Zero2</a> H616 / 1GB DDR3 / 64GB Micro SD <br> <a href="https://www.xbox.com/consoles/xbox-series-s">Xbox Series S</a> 1TB <br></p> <h4>操作系统</h4> <p><a href="https://www.microsoft.com/zh-cn/windows/get-windows-10">Windows</a> LTSC 2021 (21H2) <br> <a href="https://www.debian.org/">Debian</a> trixie | kernel 6.12 服务器用 <br> <a href="https://www.apple.com/ios/">iOS</a> 15.6 (iPhone 12) 17.7 (iPhone SE 2) <br> <a href="https://www.apple.com/ipados/">iPad OS</a> 17.3.1 (iPad mini 5) <br></p> <h3>关于本站</h3> <p>想想也是得在这个页面写一点关于本站的信息</p> <h4>博客后端</h4> <p>建立初期使用 <a href="https://github.com/hexojs/hexo">Hexo</a>,使用过 <a href="https://github.com/Siricee/hexo-theme-Chic">Chic</a> 主题,很长一段时间在使用 <a href="https://github.com/importantimport/urara">Urara</a> 模板,现在用的是自己写的 <a href="https://gitea.trle5.xyz/trle5/tplate">tplate</a></p> <h4>评论系统</h4> <p>目前支持基于 <a href="https://docs.github.com/zh/discussions">GitHub Discussions</a> 的 <a href="https://github.com/">Giscus</a> 以及基于 <a href="https://webmention.io/">webmention.io</a> 的 <a href="https://indieweb.org/Webmention">Webmention</a>,由于我换成了自己的博客模板,显示信息的部分就交给 <a href="https://github.com/kwaa">kwaa</a> 开发的 <a href="https://github.com/importantimport/seia">seia</a> 插件了</p>

2026/3/8
阅读更多