普通视图

发现新文章,点击刷新页面。
昨天以前Hubert's Blog

自定义 golang 仓库的 module / 库名

作者 Hubert Chen
2026年3月8日 08:00

如果你写过 golang,那你应该能在导入库时,注意到库名大多都是以 github.com 开头,跟一个链接差不多,复制到浏览器里也能直接访问。但有时也能看到一些与众不同的库名,例如 gopkg.in/yaml.v3go.uber.org/zap

你可以点开上面的两个链接看看,就会发现它们在 pkg.go.dev 中指向的代码仓库都托管在 GitHub,而它们却又不使用 github.com/user/repo 这样的库名,这是怎么做到的呢

大概的流程

golang 对这个实现方法在文档中有一个段落,可见:cmd/go #Remote import paths

来自 GitHub 的仓库

简要的来说,当你在终端里运行 go getgo install 命令时,golang 会尝试去解析你传入的库名,如果库名是 github.com/<user>/<repo> 这样,它可以直接识别到这是一个 GitHub 仓库,就会走一套标准的流程,检查它是不是一个 golang 库,且这个库名与代码仓库的 go.mod 文件中定义的 module 名称相符,顺利的话就可以正常下载了

如果不用定义的奇怪库名

你可能会好奇,如果类似 gopkg.in/yaml.v3 这样的库名,而我们知道它的代码仓库托管于 GitHub 中,我们能不能绕过这个奇怪名字,像普通托管在 GitHub 的 golang 项目那样下载它呢?

# 这里在加了后缀 /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

显而易见,并不行。通过 go getgo install 命令下载某个库时,它的库名必须与 go.mod 文件中定义的 module 名称相同,否则就是下不了

其他自建的版本控制服务

不来自 GitHub 也没关系,golang 也设计有一些回退方法,这时就会根据库名来判断像不像一个 URL,并尝试向这个 URL 发送 HTTP 请求,如果请求的是一个自建的 git 或其他版本控制服务的网页 URL,你的自部署服务大概率会正确的为 golang 提供信息,后续的流程就跟上面差不多了

当然这种情况不多见,更多时候应该是在企业内部的使用场景?

自定义 golang 项目的库名

如果要自定义一个 golang 项目的库名,那我们最少需要一个可以存放 HTML 文件,支持 HTTPS 且可以公开访问的域名。用一些平台提供的二级域名也可以,至于代码仓库托管在哪都可以

首先,你要确定你的库名是什么,它的开头应该是你的域名,然后跟着你的仓库名称,例如下方的演示

我有一个 golang 仓库 https://gitea.trle5.xyz/trle5/tplate
不修改的话,库名就应该是 gitea.trle5.xyz/trle5/tplate
也就是去掉了开头的 https://

你可以看到,它同时包含了二级域名,用户名和仓库名,这显得它又长又难记
同时还要避免冲突,最后决定的库名就是 trle5.xyz/gopkg/tplate

理论上也可以直接拿一整个二级域名作为库名,就是 tplate.trle5.xyz 这样,但我没试过具体行不行

修改原来的库名

如果你之前跟我一样不知道 golang 库名的命名规范,那你的库名大概就是一个单词,而且 golang 的拓展并没有提供一键重命名库名的方法... 于是,只能先修改 go.mod 文件开头的 module 名称,再看文件的报错逐个替换了

注意:如果你之前发布过 tagrelease,那你修改库名后,还要推送一个新的 tagrelease,不然你在使用 go getgo install 时,它会默认获取 latest 版本的内容,修改库名后最新的一个 commit 版本并没有包含此次更改,就会出现类似 前面 的错误

创建包含特殊属性的 <meta> 标签

能让 golang 从一个 HTML 页面中识别仓库信息的关键在于其中的 <meta> 标签,golang 不关心这个 HTML 页面里有什么其他内容,只要有对应的信息就行了,像 GitHub 上的 golang 仓库默认都会包含这个标签和对应的属性,下方的仓库 URL 填写你存放代码仓库的网页链接,代码仓库中 go.mod 文件中的 module 名称必须跟你填写的这个库名相同

<meta name="go-import" content="<库名> git <仓库 URL>">

下面是一个完整的示例,我把它保存为一个名为 tplate.html 的文件,作为资源文件存放到了我的网站里,访问路径就是 trle5.xyz/gopkg/tplate

<!doctype html>
<html>
  <head>
    <meta name="go-import" content="trle5.xyz/gopkg/tplate git https://gitea.trle5.xyz/trle5/tplate.git">
    <meta name="go-source" content="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}">
  </head>
  <style>body { color: #c8c8b9; background-color: #14140f; } a {color: #b9d282;}</style>
  <body>
    <p>go get trle5.xyz/gopkg/tplate</p>
    <a href="https://gitea.trle5.xyz/trle5/tplate">https://gitea.trle5.xyz/trle5/tplate</a>
  </body>
</html>

里面其实很多东西都是多余的,这里是参照了 Gitea 的实现而修改的文件还加上了一些信息和链接,你可以编辑好 <meta> 标签,塞到任意一个 HTML 页面的 <head> 标签里都可以

注意:go.mod 文件<meta> 标签属性对应 URL 的三个库名必须完全相同,即:

  1. go.mod 中库名为 example.com/package
  2. 浏览器中 example.com/package 可正常访问
  3. example.com/package 页面为 HTML 文档,且其中包含上方的 <meta> 标签
  4. <meta> 标签中的属性已经 按照需求 正确填写了

之后确保 CDN 缓存之类的不会干扰你的请求,就可以尝试使用新的库名来导入或安装你的项目了


2026-03-08 更新:

由于想到要不直接往文章里塞 <meta> 标签就好了,这样也不用担心文章冲突,改了一下博客模板发现还算简单,于是就直接塞到文章里了

但是似乎因为 trailing slash 的缘故,可能还需要改一改,例如目前 trle5.xyz/tplate 的信息实际上存储在 /tplate/index.html

但 Cloudflare Pages 自动把 /tplate 的请求重定向到 /tplate/index.html, pkg.go.dev 和 go 命令行也能正确抓取到信息,先暂时不动它吧... 改了

new tplate

作者 Hubert Chen
2025年11月20日 08:00

起因

我的 Pixelbook Go 的 Linux 虚拟机在某一天再次(是的,很多次)炸掉了,于是我开始将更多的开发工作挪到台式机上。但是呢,我的台式机配置依然还是 8 GB 内存。

是的,我那台 8 GB 内存的台式机已经没法跑起 Urara 模板了,在构建时会因为内存不够而失败 但这并不是我这么久没有更新的原因

时间间隔太久,我也忘了是什麽时候了,我觉得 @kwaa 写的 lume_theme_shiraha 很合我的胃口,MD3 自适应配色加上样式,非常的喜欢

后面因为对 Pixelbook Go 的逐步厌恶,我反倒去买了台只有 8 GB 内存的笔记本(ThinkPad X1 Carbon Gen 4),它给我的印象还不错,至少比前面那台能干的活多... 本来我想在这里放一个 Pixelbook Go 的链接,因为我真的喜欢它的设计,但是 Google 似乎把它从商店里下架了

但是呢... 我发现在跑 lume_theme_shiraha 时,deno 依然是得吃掉 4 GB 的内存,这是 JavaScript 的原罪吗...

现在

我于是想写一个尽量不用到 JavaScript 的博客生成模板(至少在构建时不能用),同时它还需要支持我的一些奇怪想法,于是就写,如果你能看到这个文章,那我就是在写,或许已经写好一大半了

我打算先支持一下完整的 Markdown 文章解析,再考虑其他的附加东西,最后替换掉正在使用的 Urara 博客,当然还有一大堆事情要做

⚠ 注意:此项目不遵循任何网页设计规范,您可能会遇到以下各种情况

  • 主容器不遵循 768px 最大宽度
  • 可能并不适合人类观看的配色
  • 乱用字重和行高

前期

我选了 go 语言来写这个东西,毕竟我就会两门语言,要是用 C 写的话那就不知道要写多久了(说实话也很久没写了)

其次就是我在使用 Gitea 时发现,它用到了一种叫 .tmpl 的文件,只要去仓库里把对应目录下的 .tmpl 拖到对应的目录,再修改其中的内容,就可以直接更改网页的布局,不需要构建甚至重启 还是要的,但可以做,感觉非常的神奇

但我上手了几次后,发现它的逻辑确实是有一点难懂的,如果去看 Gitea 的模板的话,它也不是按照引用 template 来嵌套组合网页的,而是从头到尾拼出来的,也就是 header + 某些组件 + 实际内容 + footer 这种类似字符串拼接的做法,其中 headerfooter 的标签并不是完全闭合的,当案例去看很误导人怎么用

而且 .tmpl 没有静态检查拓展,语法之类的就问 GPT 去吧

文章解析器

一开始我只使用了 lute 作为 Markdown 解析器,然后我发现它似乎没有什么好的方法可以在将 Markdown 解析成 HTML 的过程中将类或者 ID 选择器加到 HTML 标签中,于是开始邪恶的将 YAML 作为文章源文件(对的,现在还留着,现在这篇文章就是用 YAML 写的 放弃了)

但我尝试写了一会后,发现这样跟手搓 HTML 似乎没什么区别,过了一阵子又改回来了。但样式的问题解决了,CSS 的类型选择器 也不是不能用,而且不用往 HTML 标签里单独塞东西感觉还不错

期间搬了自己几篇文章试试效果,就是不知道为什么不能解析 Markdown 的 Front Matter 部分。没有什么想法,于是跑回去写 bot,由于配置文件我也用的是 YAML,结果越看越眼熟...

这个 Front Matter 部分怎么长得跟 YAML 有点像,而且 Front Matter 前后的 --- 符号似乎也是 YAML 的单文件分隔符,询问神奇的 ChatGPT 后,得到了肯定的答复,于是简单写个解析器区分 YAML 和 Markdown 部分单独解析,文章信息就好了

代码高亮

本来是不打算做代码高亮的,原因很简单,我写不出来

但是呢,lute 支持代码高亮,虽然中途发现样式似乎有奇怪的问题,但后面改了一下,效果还不错,但配色跟我的主题看上去不是很好,现在还没有改的打算

Spoiler / 遮罩

由于 lute 好像不支持 Markdown 的 Spoiler 语法(即 || 遮罩内容 ||),而我之前又有在部分文章中使用到了这个语法。找了一圈 HTML 中也没有对应的标签实现

当然你能在这个文章中看到这个语法,就代表我用了一些奇怪的实现方式 因为我把 <mark> 标签夺舍成 Spoiler 语法了,实际上就是 ==遮罩内容==

这可能会导致在一些 RSS 订阅的阅读器中降低一些阅读体验,因为没找到什么好方法,而且在手机上,必须得长按选中才能看到实际的内容

文章目录和章节

lute 好像没有方法在渲染 Markdown 后拿到一些章节数据,不过还是提供了章节标题锚点的功能,见章节标题右边那个小图标

改了一下,靠 JavaScript 实现了类似 lume_theme_shiraha 的标题链接锚点效果,本来也想写到标题前面,但是因为我设定的 body 填充范围小了些,放到外面在移动端上不是很方便,就顺着放到右边了

一开始是靠作为 lute 标题链接锚点的伪元素实现的,但这样其实还是会留着 svg 元素,标题多了浪费流量,后面问了问 GPT 才给出一个用 JavaScript 代码自动查找元素并添加子元素的方法,但这也导致了如果禁用 JavaScript 那就没法显示出这个按钮

也因为这个,所以文章目录不是很好写,或许可以写一些正则表达式,逐行匹配 heading 标签来尝试生成目录,这个缓一缓也没什么问题了...

我还想设计成每个段落都像代码编辑器里那样可以折叠显示,这样相对来说可能方便些?不过感觉很难做

样式

由于 前面说到 lume_theme_shiraha 很合我的胃口,就打算照着样子做一个(不过就不是最新版那个样子了),还有部分设计借鉴了 Urara,非常感谢 kwaa

当然前者是有 MD3 按图片取色的,这个我尝试从网上找一个 MD3 取色的 go 语言库,完全没找到于是放弃,就固定两个配色就好了...

个人偏好

期间还掺杂了个人偏好的样式,例如我觉得全屏看网页时 768px 的主容器宽度未必显得两侧太空了,于是在窗口大小大于 1536px 时,主容器宽度会拓展到 1024px。也不至于出现 21:9 的屏幕读一行字需要头从左往右转的情况

由于没有使用 css 库之类的,样式全都是自己写的,还可以把样式全部写进 HTML 的 <head> 标签里,这样只需要按 Ctrl + S 就能按照原样保存的网页

不过文章里有引用网络资源的话就还不行,或许能想个办法在构建静态网页时把引用的网络资源都一起扔进 HTML 里。但这样可能会因为重复加载相同的样式和资源而消耗多余的流量甚至拖慢加载速度,这个就之后再说了

按需样式加载

好像前端那些样式库之类的东西,都会根据使用到的样式来按需构建样式表文件,而我思考了一下,好像做起来很难,但每个页面都加载用到的全部样式又比较浪费流量,思考了一段时间后我选择了另一个方法

把全部的样式文件,看哪些组件会用到哪些样式,将其分别存放到对应文件夹中,在构建时存储到变量中,当调用这个组件时,自动在组件前附加样式

本来还想把样式都丢在 <head> 标签里,但是在 .tmpl 模板中进行操作时无法更改外部的值,好在测试都正常,就暂时用吧

文章列表

想到之前使用 Urara 时会将题图放到文章列表中,于是自己试了试也做一个,发现效果很难看,甚至想过要不还是不显示图片了

后面又乱折腾了一阵,最后是作为文章列表项目的背景图像显示的,并叠加了一个背景色到透明的渐变来模拟遮罩效果,在深色模式下观感可能还可以,但浅色模式下可能就不太行了

而且图片的显示区域是由设定文章描述高度来撑开的,想不到什么办法了...

通过 PageSpeed Insights 测试了一下,这个题图实现方式还有个应该应用 fetchpriority="high" 的建议,但我这个是 <div> 标签的 background 样式,添加属性也没效果,就暂时先这样了

文章分类和标签

在之前使用 Urara 的时候是有从其他仓库里拿过一个 sections 插件(见 Interstellar750/hexo_s/urara 的 README 部分),可以以某个分类来显示其下的文章,这个代码在我的 Urara 库中还有保留,但是 @jiwaszki 似乎转向使用 hugo 来搭建博客,原来的代码就不得而知了...

好在这个写起来也不算太难,其实仔细想想跟 tag 也没差多少,就是分类只能指定一个,而标签能指定多个,最终都是靠标签或分类来索引符合条件的文章而已

由于它们很像,索引过程也都是用的文章列表模板,我把它们都放到同一个页面去了,然后再遍历生成 HTML,分类的的 URI 就是 host/category/<分类名>,标签就是 host/category/_<标签名> (前面多一个下划线)

在分类展示页里,分类以 grid 的形式显示在前面,显示分类名、最后发布的文章时间和其下的文章数量。后面部分就留给标签,如果有某个标签包含多个文章,也会显示计数

不过相比 Urara 就不能按分类 + 标签逐个筛选了,只能一个个看

网页交互

RSS 订阅 / Sitemap 数据

一开始我觉得 HTML 也是一种 XML 嘛,就直接当做 .tmpl 文件,进行一个模版的写,但是由于开头的 <?xml version='1.0' encoding='utf-8'?> 不是一个正常的 HTML 标签,就会导致被转义掉,就算用 template.HTML() 套住也不行,它在执行模板时就被转义了,没法被正常识别成 XML 文件

中途想了各种奇怪方法来解决这个问题,最后在写文章的时候突发奇想:

既然就开头那一行有问题,那我先把它去掉,执行完模板后再把开头作为字符串把它拼起来不就好了

于是现在就只需要在使用时填一下 "atom" 参数就好了,舒服

途中还看了看 AtomRSS 的区别,我一开始还以为 RSS 是 Atom 的升级,没想到是反着来的

后面顺带把 sitemap 也做了

链接预览

在写了一半发出去让群友们不得不品鉴时,发现好像没有链接预览,试了试几个博客链接,最后发现是 <meta> 标签的问题

途中看了各种网站,最后的结论大概是得加 <meta property="og:description" content="描述"> 这种标签,但是总感觉不太对劲,我在 MDN 甚至都没看到 meta 标签有 property 这个属性...

还尝试过为文章生成链接预览图片,但尝试了一会后发现设计出来的图相当难看... 暂时搁置了

评论系统

好在我使用 Urara 时并没有添加太多的评论系统,只用到了 GiscusWebmention,这让我的迁移过程变得比较轻松

Giscus 本来就提供 JavaScript 代码的使用方式,而 Webmention 显示嘛,用 kwaa 写的 seia 就好了

提交 Webmention 链接的话,去了解了一下 API 端点,请 ChatGPT 写了一份代码,就好了

为 Chrome OS 设置代理

作者 Hubert Chen
2024年8月6日 08:00

⚠ 文章未完成

Chrome OS 因为安全原因逐步隔离了用户空间和 crosh 终端

这导致了原本固定映射至用户下载文件夹的 /home/chronos/Downloads 目录已经消失了

于是现在找不到一个稳定的目录可以直接将 sing-box 的二进制文件转移到 crosh 中的 /usr/local/bin/ 目录中(可能有,但我心灰意冷懒得找了)

而如果在 crosh 中直接使用 curl 从 GitHub 上下载文件,大概率会因为 DNS 污染等原因下载失败,没有一个好用的办法...

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

点击来帮助 Chrome OS 夺回属于它的内存

在某一天调式程序时,我发现请求的 API 似乎迟迟没有响应,想着是不是节点出问题了?当我尝试打开 Android 上的代理软件时,我意识到并不是网络问题,而是这个 Android 虚拟机,在只跑了一个代理软件的情况下,卡死了...

保存代码进度再重启设备后,Android 虚拟机正常了,继续写代码调程序,但那天晚上我意识到,我似乎启用这个 Android 虚拟机,为的只是跑一个代理软件?于是在第二天我备份好 Android 虚拟机中的必要数据后就把它删掉了。然后我在诊断程序里看到设备的空闲内存从日常的 2GB 变为了... 12GB

剩下的就不多说了,现在 Chrome OS 夺回了属于它的内存,只需要解决网络代理问题就可以了

如果你的网络情况还不能在脱离了 Android 环境中的代理软件下访问网络,那请先不要着急删掉它

删掉了 Android 容器后,就得考虑选择替代的代理方式了,目前大概有四类设置代理的方式,在下方表格中按配置难易度从低到高排序,还有能代理到的范围和优缺点

代理类型 Chrome crostini crosh 优点 缺点
Wi-Fi 代理 简单方便,可连本机或其他设备 代理范围过小,Linux 虚拟机吃不上,每个 Wi-Fi 都需要单独配置
WireGuard 代理范围广,有系统 UI 控件可快捷切换 需要进入 crosh 添加配置,协议因素有速度劣势
Tun 代理范围广 需要在 crosh 中配置代理软件,可能还会有些奇怪问题
TProxy 暂时想不到 代理范围一般,Linux 虚拟机会直接没网(不只是没代理)

非侵入式(不修改 Chrome OS)

我们有两种不需要安装任何软件就能添加代理的方式,它们就是 Chrome OS 自带的 WireGuard 和比较差劲的 Wi-Fi 代理

1. WireGuard

通过 WireGuard 设置代理可以覆盖到浏览器、Linux 虚拟机和 crosh,还有系统 UI 控件,方便又好用。

但是系统自带的图形化 WireGuard 配置添加工具有 bug,必须要在 crosh 中通过命令来添加配置,这代表你必须要清除所有数据,开启开发者模式后才能添加 WireGuard 配置 似乎并不需要,请手动试试能不能通过下方的快捷键打开 crosh,如果可以的话就不需要

此教程来自 ChromeOS Flex: Can’t save Wireguard config (FIX) – Tech Blog (Web Archive link)

由于 Cloudflare 的 Warp 现在被 ban 的厉害,比较推荐自建服务端

首先确保你已经为你的 Chrome OS 设备启用了开发者模式,之后按 Ctrl + Alt + T 来打开 crosh,再逐行运行以下命令,注意根据你的 WireGuard 配置替换掉对应的内容:

wireguard new <名称>
wireguard set <名称> local-ip 10.8.0.9
wireguard set <名称> private-key # 运行这行命令后粘贴密钥,回车保存
wireguard set <名称> peer <公钥> preshared-key # 同上,但如果没有预共享密钥则不用运行此行
wireguard set <名称> peer <公钥> allowed-ips 0.0.0.0/0
wireguard set <名称> peer <公钥> endpoint <端点 IP:端口>
wireguard set <名称> peer <公钥> persistent-keepalive 0
wireguard show # 查看添加完成的配置

添加完配置后,你应该会得到类似以下的输出:

name: wireguard-profile
  local ip: 10.8.0.9
  public key: iJbPYu1dCc1VVU7o6OS5uWhnni9iXCi1nuGkwdsIPn0=
  private key: (hidden)
  name servers: 8.8.8.8, 8.8.4.4

  peer: QiTfo+u2brfoTh5BHQKbU/Rt/OJ7MAQMO0+pMEYNxRg=
    preshared key: (hidden or not set)
    endpoint: 127.1:5300
    allowed ips: 0.0.0.0/0
    persistent keepalive: 0

其中的 public key、peer 以及 endpoint 可能会与上方显示的不同,因为这几个参数取决于你的 WireGuard 配置,然后你就可以到设置的网络板块里启用刚刚添加的配置,试试能不能正常代理流量了

2. Wi-Fi 代理

Wi-Fi 代理更适合只用浏览器上网的用户,因为代理不到 Linux 虚拟机,可以应急用用,长期使用还是推荐选择其他方式

Wi-Fi 代理配置方法不算难,只需要在设置中依次点击 网络 > Wi-Fi,找到目前已连接的 Wi-Fi 名称并点击它就可以查看网络详情信息,再点击最下方的 代理 选项,把连接选项改为 手动配置代理,就可以配置 Wi-Fi 代理了

接下来就是填入 HTTP/SSOCKS 代理主机的 IP 以及端口了,如果你的代理软件支持在一个端口上使用多个协议,还可以把 对所有协议使用同一代理 打开,那样只需要填一个主机的 IP 和端口就足够了,至于下面的 不要对以下主机和域使用代理设置 直接留空,如果你知道这方面的知识,就看你想怎么来了,填完信息记得点击保存

只要你填入的 IP 和端口正确,且对应的设备上运行着负责代理流量的服务,那么这个时候 Wi-Fi 代理就已经生效了,去测试一下吧

侵入式(在 Chrome OS 中安装代理软件)

如果要像 Linux 发行版那样在 Chrome OS 里安装代理软件,那么首先你得给你的 Chrome OS 设备启用开发者模式

注意不是修改 设置 > 关于系统 > 版本 里的开发者版本

具体怎么启用开发者模式我这里就不写了,只讲启用开发者模式后如何将代理软件安装到 Chrome OS 中

如果你选择安装侵入式的代理软件,你就可以选择多种方式来将 Chrome OS 连接到代理,这里首要推荐的是支持 Tun 模式的 sing-box

sing-box

折腾了许久,在 Chrome OS 中似乎只有 sing-box 能使用 Tun 模式来代理设备的全部流量,根本原因大概有两个:

  1. 似乎 Chrome OS 的系统网络栈不支持执行 L3 到 L4 转换,只有在 sing-box 入站(inbounds) 配置中将 Tun 的 stack 参数手动设定为 gVisor 才可以正常转发流量
  2. 同时需要将 sing-box 路由(route) 配置中的 default_interface 参数设定为 wlan0,这个值对应的是给机器提供网络的网卡设备,我的 Pixelbook Go 没有网线接口,我也没有能插网线的拓展坞,没法测试连接网线后是否需要修改。有能力的话请自己到 crosh 里运行 ifconfig 命令判断是否要修改

下方是一份针对 Chrome OS 的 sing-box 配置,配置好了 Tun TProxy 和 HTTP 代理入站,Yacd Web UI 控制台,DNS 和去广告规则集,使用时请手动替换掉出站(outbounds) 中的示例节点:

点击查看配置
{
  "log": { "level": "info", "timestamp": false },
  "experimental": {
    "clash_api": { "default_mode": "rule", "external_controller": "127.0.0.1:9090",
      "external_ui": "ui", "secret": "", "external_ui_download_detour": "",
      "external_ui_download_url": "https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip"
  },
  "cache_file": { "enabled": true, "store_fakeip": false } },
  "dns": {
    "servers": [
      { "tag": "proxyDns", "detour": "proxy", "address": "https://1.1.1.1/dns-query" },
      { "tag": "localDns", "detour": "direct", "address": "tls://120.53.53.53" },
      { "tag": "block", "address": "rcode://success" }
    ],
    "rules": [
      { "rule_set": "geosite-category-ads-all", "server": "block" },
      { "outbound": "any", "server": "localDns", "disable_cache": true },
      { "clash_mode": "direct", "server": "localDns" },
      { "clash_mode": "global", "server": "proxyDns" }
    ],
    "strategy": "prefer_ipv4"
  },
  "inbounds": [
    { "type": "tun", "stack": "gvisor",
      "auto_route": true, "strict_route": true,
      "domain_strategy": "prefer_ipv4", "mtu": 9000,
      "inet4_address": "172.114.0.1/30", "inet6_address": "2001::1/64",
      "sniff": true, "sniff_override_destination": true,
      "inet4_route_exclude_address": [ "0.0.0.0/8", "10.0.0.0/8", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.168.0.0/16", "224.0.0.0/4", "240.0.0.0/4" ]
    },
    { "type": "tproxy", "tag": "TPROXY-IN", "listen": "0.0.0.0", "listen_port": 7895, "sniff": true, "sniff_override_destination": true },
    { "type": "mixed", "listen": "127.0.0.1", "listen_port": 2080, "sniff": true, "users": [] }
  ],
  "outbounds": [
    { "tag":"proxy", "type":"selector", "outbounds":[ "auto", "direct", "outbound_hy2" ] },
    { "tag":"auto", "type":"urltest", "url": "http://www.gstatic.com/generate_204", "interval": "10m", "tolerance": 50, "outbounds":[ "outbound_hy2" ] },
    {
      "type": "hysteria2", "tag": "outbound_hy2",
      "server": "114.51.4.191", "server_port": 9810,
      "password": "example_password",
      "tls": { "enabled": true, "server_name": "exmaple.com", "insecure": true }
    },
    { "type": "direct", "tag": "direct" },
    { "type": "dns", "tag": "dns-out" },
    { "type": "block", "tag": "block" }
  ],
  "route": {
    "default_interface": "wlan0",
    "final": "proxy",
    "rules": [
      { "protocol": "dns", "outbound": "dns-out" },
      { "rule_set": "geosite-category-ads-all", "outbound": "block" },
      { "clash_mode": "direct", "outbound": "direct" },
      { "clash_mode": "global", "outbound": "proxy" },
      { "domain": [ "clash.razord.top", "yacd.metacubex.one", "yacd.haishan.me", "d.metacubex.one" ], "outbound": "direct" }
    ],
    "rule_set": [
      { "tag": "geosite-category-ads-all", "type": "remote", "format": "binary", "download_detour": "direct",
        "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/category-ads-all.srs"
      }
    ]
  }
}

配置文件有了,你可以决定是否要继续操作,接下来的步骤如果操作不当可能会炸掉你的系统

首先判断一下你的 Chrome OS 设备的架构,按 Ctrl + Alt + T 打开 crosh,使用 uname -m 命令来查看硬件架构:

crosh> uname -m
x86_64

由于我的 Pixelbook Go 使用的是英特尔(Intel) 的处理器,所以这里显示的是 x86_64,如果是 AMD(超威半导体) 的处理器,那应该也是 x86_64,也有可能是 amd64

如果你的 Chromebook 使用了高通(Qualcomm) 或者联发科(MediaTek) 的处理器,那可能会显示 arm64 或者 aarch64

接着前往 SagerNet/sing-box 的最新发布页面,根据你的设备架构来拿到最新的 sing-box 的下载链接

如果设备架构是 x86_64amd64,那就选 sing-box-<版本号>-linux-amd64.tar.gz

如果设备架构是 aarch64arm64,那就选 sing-box-<版本号>-linux-arm64.tar.gz

建议打开网站后按 Ctrl + F 进行搜索,先搜 linux-,然后根据你的设备架构接着输入 amd64arm64,最后唯一高亮的那个就是你需要的,右键那一行字,再点击复制链接地址

注意是点击 复制链接地址,不是 复制,否则你复制到的链接地址是错误的

接下来按下按 Ctrl + Alt + T 打开 crosh,逐行输入以下命令并回车,将其作为一个命令脚本保存到文件中

shell
cd /tmp
# 这篇文章完成时,最新的版本是 1.11.7
echo 'curl "https://github.com/SagerNet/sing-box/releases/download/v1.11.7/sing-box-1.11.7-linux-amd64.tar.gz" > sing-box.tar.gz' > download_sing-box.sh
sh download_sing-box.sh && tar xzf sing-box.tar.gz &&

隐藏站点的源服务器 IP

作者 Hubert Chen
2024年4月5日 08:00

从不开 CDN,域名 DNS 直接解析到 IP,再到服务器被打到空路由后的总结

为什么要隐藏源服务器 IP

首先我们要看看浏览器访问一个站点时,这个请求会如何到达源服务器:

  1. 浏览器获得用户输入的域名,首先向 DNS 服务器发送查询请求
  2. DNS 服务器收到了请求,解析到了域名对应的 IP,返回给浏览器
  3. 浏览器得知了源服务器的 IP 地址,接着继续向源服务器发送请求

DNS 的作用就不多说了,简单来看就是放域名进去,就可以吐出域名对应的 IP

这个过程中,浏览器就已经知道源服务器的 IP 了,怎么知道的呢?当然是 DNS 告诉它的

但 DNS 是怎么知道源服务器的 IP 呢?不是什么魔法,是站点主人自己告诉 DNS 的,不然没了 DNS,想访问站点只能记一串 IP,很不方便

现在我看某个人不爽,想把他的网站做掉

当然我不提倡任何网络攻击,这里只是做一个例子

上面我们知道了请求是如何到达源站的,最简单的方法,就是一秒给他发几千个请求甚至更多,就能起效果。于是立刻手搓了一段代码,一秒发五千个请求,这个叫分布式拒绝服务(DDoS)攻击

由于服务器的配置并不是很高,处理不来我们发送的这么多请求,一段时候后,服务器的主人发现了不对劲,赶紧把 DNS 解析删除了,打算等我们不打他之后再重新添加 DNS 解析,让网站恢复访问

此时,虽然 DNS 解析被删除了,但服主在被打之前在 DNS 里设置的 IP 已经被我们请求过了,我们早已知道了源站 IP。于是我们乘胜追击,把发送请求的域名换成 IP,接着打:

在我们的不懈努力下,服务器的占用爆满,服主只能关机跑路
就算服主重新开机,迎接他的依然是 100% 占用,跑不掉

到这里我们已经知道 DNS 与 IP 公开绑定的后果了,就算站点不能通过域名访问了,只要服务器不关机,那还是能通过 IP 访问的,不做些什么,站点就只能停了

所以,我们要在确保通过域名可以正常访问的情况下,保证源服务器的 IP 不被泄露

有哪些办法可以隐藏源服务器 IP?

我们先代入到前面的攻击事件中服主的视角,看看有什么方法可恢复站点访问,还能让他打不到我们

换 IP

一台机器想要作为站点的服务器,大部分情况下都需要被分配一个可被外部访问的 IP 地址,但它是可以更换的,可能会需要一些成本

更换了 IP 后,我们可以的服务器就可以开机了,因为攻击者并不知道我们服务器新的 IP 是多少,在不设定 DNS 解析的情况下,服务器是暂时安全了。但如果再次把域名解析到新的 IP,那他还是跟前面一样,只需要访问一次就能知道我们的新 IP,服务器就会继续挨打

我们也可以更换 IP 的同时更换一个域名,不过这样可能会损失之前积累的用户。如果你将更换域名的消息告诉了之前的用户,而攻击者潜伏在用户之中,那么你的服务器可能会再次遭殃

若网站之前有被搜索引擎收录,更换后的新域名再次被收录,那攻击者可以通过搜索引擎得知更换后的域名,DNS 再返回对应的 IP,那服务器同样会遭殃

上高防 / 升级服务器

只要你的配置够高,他就没法把你的服务器干掉线,只是看你的钱包能不能受得住。DDoS 的攻击和防御成本并不是一个量级的,一般的小站点可没有这个资金去买高防,升级服务器的性价比也很低,这是一个很糟糕的解决办法

使用 CDN

本文章的主角,能让 DNS 解析不会给出你的源站 IP,同时还能保证域名可以正常访问,它是怎么运作的呢?

CDN 翻译过来就是内容分发网络,在 DNS 层面,它可以将 DNS 解析出来的 IP 从你的源站 IP 换为来自 CDN 的 IP,避免 DNS 直接暴露源站 IP

DNS 解析出来不是源站 IP,那怎么获取我的站点内容呢?

按照流程,在 前面 所说的 DNS 解析出 IP 进行返回的过程中,使用 CDN 后,解析出来的 IP 是来自 CDN 的。接下来的请求部分,用户会从向源站 IP 发送请求变为向 CDN 的 IP 发送请求,这里 CDN 需要负责接受请求,然后再向源服务器发送请求,过程如下:

# 没有使用 CDN 的请求
用户发送请求     >   源服务器收到请求
源服务器发送响应  >      用户收到响应

# 使用了 CDN 的请求
用户发送请求    >        CDN 收到请求
CDN 转发请求    >    源服务器收到请求
源服务器发送响应  >      CDN 收到响应
CDN 转发响应    >      用户收到响应

可以看到,开启 CDN 比不开启 CDN,用户每个请求都会先到达 CDN,再由 CDN 转发到源服务器,源服务器处理完得到响应之后,再发送给 CDN,CDN 再将回应发送给用户

看上去多了两个步骤,麻烦了不少,但我们需要的隐藏源站 IP 功能不是实现了?

不过要注意,CDN 很多时候只能转发 HTTP/S 请求,例如你本来用你的域名直接解析到 IP,使用 git 克隆仓库时可以使用域名替换服务器 IP,但开启 CDN 后,就不能这样了,你可能需要找一些其他方法

还没完

我们选择了最实用的 CDN 方法来隐藏了源站 IP,但还有一些需要注意的事情

  1. 如果你在没开 CDN 之前就被打了,只开 CDN 是不行的,你还需要同步更换 IP。CDN 只是阻断了通过 DNS 获取源站 IP 的方法,但你的 IP 早就在开 CDN 前泄露了,此时攻击者不会和你走流程,直接对着 IP 继续打,服务器还是得遭殃
  2. 我没有开过 CDN,但我也并没有被打过,看了文章我现在感觉有必要开一个 CDN,还来得及吗?
    没问题,但有隐患。网上有很多专门扫域名扫 IP 的,可能已经记录了你的 IP,但情况并不坏,只要没人刻意去查,是找不到的,就算找到了,你简单换一下 IP,那些记录也就失效了

避免源服务器暴露站点特征

截止到这里,防止 DNS 泄露源站 IP 的方法其实已经解决了,不过为了安全,我们还是要配置一下源服务器上的反向代理,避免网上那些扫 IP 的服务暴露了我们在 CDN 后的源服务器 IP

排查暴露的端口

首先,我们要检查一下直接访问服务器 IP 能获得什么,请按照下面的列表排查一下自己服务器:

  • 在浏览器中通过 IP:80IP:443 能否直接访问到你的 HTTP/S 服务
  • 如果你有其他非标准端口的服务,使用 IP:端口 是否能直接访问
  • 如果你有部署 docker 或其他容器类服务,请检查容器的端口映射是否暴露在了公网
  • (可选)去 Censys 搜搜你的 IP 中有哪些被记录的端口和服务
暴露的 80/443 标准端口

如果你的 HTTP/S 服务能够通过 80/443 端口访问,那说明你的服务就直接暴露出来了,就算攻击者因为 CDN 中继保护还不知道你的 IP,但 80/443 是标准的 HTTP/S 端口,最简单的 IP 扫描也不会放过这两个端口,只要被扫到,基本就会暴露对应的数据

你可能会好奇,不是开了 CDN 吗?但 CDN 也是通过 80/443 端口才能转发服务器与客户端之间的请求,CDN 能访问到的数据,通过 IP 访问也可以,而直接通过 IP 访问则会绕过 CDN。如果一个 IP 访问后显示的网页数据和某个域名一样,那不用猜,这个 IP 就是域名后面对应的源服务器 IP,这时你的源服务器 IP 也就暴露了

还有上面说到的 Censys,有些人可能觉得我去上面搜自己的 IP 不就是主动暴露自己吗,确实有这个风险,但你可以去搜一下你的域名,有时候你的 DNS 解析或是服务器设置的证书中的域名也可能会在上面暴露 IP 与域名的关系

暴露的非标准端口

第二则是非标准端口的服务,其实也像上面一样,没什么安全性。大一点的扫描类服务为了数据库够大、相比同行更有竞争性,基本都会把每个 IP 能扫的端口都扫一遍,设定非标准端口并没有太大作用

暴露的容器端口

最后是容器中的端口映射,一般容器与主机的端口映射是可以修改的,很多容器自部署服务为了避免端口冲突,并不会将服务端口设为 80/443,而是 3000、5000 这种四位数的端口。为了让 IP 或域名可以不加端口直接访问,需要在服务器上设定反向代理,在收到请求时,根据传入的域名,决定转发请求到哪个容器中处理

设置得当时,通过域名访问站点,不管源服务器上有多少个容器,反向代理都可以帮你正确的转发请求到对应的容器,而用户与 CDN 发送的请求始终都是通过标准的 80/443 HTTP/S 端口,这可以让我们无需为每个相同的 HTTP/S 服务单独设置一个端口,避免了暴露多个端口的问题

不过,你有按照我说的步骤去排查 docker 容器端口映射了吗?如果发现你的 docker 容器也可以使用 IP:端口 直接访问,那你就要注意一下 docker 的端口映射方式了

docker 中容器的端口映射有两种方式:暴露至公网与仅在本机暴露,下面假设容器内的端口为 2340,映射到主机的端口为 1230:

// 暴露至公网
// docker run 启动
docker run -p 1230:2340 <容器镜像>
// docker compose 配置
ports:
  - 1230:2340

// 仅在本机暴露
// docker run 启动
docker run -p 127.0.0.1:1230:2340 <容器镜像>
// docker compose 配置
ports:
  - 127.0.0.1:1230:2340

相比之下就是在 1230:2340 前多加了一段 127.0.0.1:,可能有点难看懂,这时它实际也是分为两段,127.0.0.1:1230 为主机的部分,2340 依然为容器端口,这里我们主要是限定了这个端口只允许在 127.0.0.1 访问,而 127.0.0.1 同时也对应了 localhost,就仅为本机使用

修改容器配置后,重启一下容器,再运行 docker ps 看看 PORTS 是不是由 0.0.0.0:1230->2340/tcp 变成了 127.0.0.1:1230->2340/tcp。这个时候,就无法通过 IP:1230 的方式访问到 docker 中的容器了

还有什么要注意的吗?当然,现在通过 IP 的 80/443 端口依然可以访问到服务器上的服务,跟 前面 说的一样,只要通过 IP 访问依然能得到与通过域名访问一样的数据,那还是能猜到这两者的关联,所以我们要完全切断 IP 与域名的联系,不留任何可以被关联的数据

当然肯定不是让你直接关机跑路

禁止直接通过 IP 访问

看上去很玄乎?虽然前面也提到了 CDN 能访问到的数据,通过 IP 访问也可以,但也有方法,让 CDN 可以正常转发,而直接访问 IP 不返回任何数据

此段部分内容来自 NGINX 配置避免 IP 访问时证书暴露域名 - ZingLix Blog 文章,在此感谢 ZingLix

假设我们使用的反向代理软件为 Nginx,我们原本的配置如下,其中假设自己的域名为 example.com

# 此段作用为转发 HTTP 请求至 HTTPS
server {
  listen 80 default_server;
  return 301 https://$host$request_uri;
}

server {
  listen 443 http2 ssl;
  server_name example.com;
  ssl_certificate <SSL 证书公钥文件>;
  ssl_certificate_key <SSL 证书私钥文件>;
  # 忽略了一些配置

  location / {
    proxy_pass http://localhost:3000;
    # 忽略了一些配置
  }
}

一份很普通的 nginx 配置,除了转发 HTTP 请求到 HTTPS 外,还有一段配置,作用是收到包含 example.com 域名的请求时,将请求转发到本地的 3000 端口进行处理。解析好 DNS 或同时加上 CDN,你都可以通过 example.com 域名来访问到服务器上的 3000 端口对应的服务

不过,如果你通过浏览器直接访问 IP,不管使用 http:// 还是 https:// 前缀,最后都是访问到了 443 端口,因为第一端的转发请求起了作用。而下一步就是请求 443 端口了,第二段配置的 listen 443 意思就是监听 443 端口,那么这个请求自然就传到第二段配置设定的 3000 端口对应的服务里去了,这就造成了使用 IP 能直接访问到服务器上的服务,我们可不希望这样

添加配置拒绝直接通过 IP 访问时的请求

那怎么办?我们可以在第一段后面添加一段监听 443 端口的配置,返回 403 或者 404?你可以现在试一下:

# 此处作用为转发 HTTP 请求至 HTTPS
server {
  listen 80 default_server;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl default_server;
  return 403;
}

# 忽略了第三段配置

然后你会不出所料的得到类似 no "ssl_certificate" is defined for the "listen ... ssl" 的错误,简单来说就是,想要监听 443 端口,你必须要设定一个 SSL 证书,当然这个证书是否有效并不重要,只要是个证书就可以

但在添加证书前,我们先看看证书里有哪些内容(来自 什么是 SSL 证书? | Cloudflare):

针对其颁发证书的域名
证书颁发给哪一个人、组织或设备
证书由哪一证书颁发机构颁发
证书颁发机构的数字签名
关联的子域
证书的颁发日期
证书的到期日期
公钥(私钥为保密状态)

看到了吗,第一行中就有关于域名的信息,这可不行,在这个配置里加 SSL 证书也会泄露我们的域名,怎么办呢?

也有办法,只要我们给一个不包含我们域名信息的 SSL 证书就可以了,毕竟 nginx 并不会检验证书的有效性,于是我们使用 openssl 生成一个私钥和证书:

# 请在 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

运行以上命令后,目录下会多出来三个文件,分别是 private.keycert.csrcertificate.crt,将其添加到前面的配置里:

# 忽略了第一段配置

server {
  listen 443 ssl default_server;
  ssl_certificate <文件目录>/certificate.crt;
  ssl_certificate_key <文件目录>/private.key;
  return 403;
}

# 忽略了第三段配置

此时再去直接访问 IP,你就会得到证书无效的错误,当然你也可以在页面上找一找,一般会有高级选项允许继续访问,就会得到 403 或 404 的错误,看你是如何设置的

不过我们是为了防止证书暴露信息,所以这是无法访问也是在我们的预期中的。点击浏览器地址栏旁边的信息按钮,这个按钮一般会显示成一个小锁或是类似设置的图标,就能看到证书无效的提示,点击查看详细信息就在弹出的窗口里可以看到里面的证书信息,这些信息就是你在前面生成证书时填写的信息

到这里,已经很难将你的源站与域名联系起来了,只要你使用 openssl 生成的证书中没有明晃晃写上你的域名,那想通过特征找到源站对应的域名,就等于大海捞针了

如果你的 Nginx 版本高于 1.19.4,你还可以使用新功能:拒绝握手

设定直接通过 IP 访问时拒绝握手

注意:这个功能和上一个方法作用类似,你只需要选择其中的一个来设置。相对而言,拒绝握手能提供更少的信息,效果更佳

这个功能需要 Nginx 版本高于 1.19.4,你可以在终端运行一下 nginx -v 检查一下是否可以使用该新功能。当然你也可以去升级 Nginx,如果无法升级或其他原因不想升级,上面的方法也足够安全了

拒绝握手的方法配置起来很简单,与上面的方法类似,只需在正常配置前添加一个监听 443 端口的服务即可,可以不指定 SSL 证书:

# 此处作用为转发 HTTP 请求至 HTTPS
server {
  listen 80 default_server;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl default_server;
  # 当配置中开启拒绝握手时,不需要添加 SSL 证书与私钥
  ssl_reject_handshake on;
}

# 忽略了第三段配置

保存配置并重启 nginx 服务后,再去浏览器里访问 IP,就可以看到已经无法通过 IP 访问到站点了,而且页面信息里也没有证书信息,可谓非常的干净

第二段配置中的 ssl_reject_handshake on 就是关键所在,只要一段配置中启用了这个功能,那么走这个通道的请求都会被拒绝握手,不会提供任何信息,所以注意不要往正常的服务配置里启用这个功能

我们安全了?暂时的

直到这里,我们开启了 CDN、关闭了暴露的端口、禁止 IP 访问以及配置了证书避免泄露我们的域名,不过还有一个方式能访问到服务,那就是我们的域名

为什么配置了怎么多,IP 都不能访问到网站了,而通过域名还可以?下面有两行命令,你可以试一试:

curl -v -k https://<你的源站 IP>
curl -v -k https://<你的源站所绑定的域名>

第一行是直接通过 IP 请求内容,第二行相同,但走的是域名来请求内容,可能还会经过 CDN。看着我们是配置成功了,IP 会无法访问,域名访问一切正常,不过,如果你试试下面这行命令呢?

curl -v -k https://<你的源站所绑定的域名> --resolve <你的源站所绑定的域名>:443:<你的源站 IP>

看似能正常从域名获取网页内容?没错,不过我可要告诉你,这里请求的其实是 IP,而不是你的域名,是不会经过 CDN 的。但我们配置了禁止 IP 访问,为什么还是能获取到站点内容呢?

依然有办法可以通过 IP 访问

先看看这行命令有哪些参数,下面的域名将被替换为 example.com,IP 被替换为 333.333.333.333,CDN 的 IP 被替换为 444.444.444.444:

curl \
  -v \ # 输出请求时的整个过程
  -k \ # 请求时跳过 SSL 检测(不会因为证书原因导致请求失败)
  https://example.com \ # 要请求的域名
  --resolve \ # 将某个域名强制解析到某个 IP(替代 DNS 的作用)
    example.com:443:333.333.333.333 # 要强制指定的数据

这么看,这行命令的作用就是:在请求时输出全部日志并跳过 SSL 检测,跳过正常的 DNS 环节,直接将域名解析到指定的 IP 地址,再向强制解析后的域名发送请求,运行后的输出类似这样:

$ 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)
# 忽略后续信息

而正常通过域名访问的输出呢?

$ curl -v -k https://example.com
*   Trying 444.444.444.444:443...
* Connected to example.com (444.444.444.444) port 443 (#0)
# 忽略后续信息

可以看到,如果指定了 --resolve example.com:443:333.333.333.333 参数,在请求时会将 example.com 直接解析到 333.333.333.333,之后请求会直接冲着源服务器去,服务器再返回站点数据,我们的服务器可能就会再次沦陷了

怎么办呢?说实话,如果攻击者到了这种拿着域名一个个试试 IP 能不能通过请求的时候,我们就只能限制哪些 IP 可以直接访问服务器了

限制哪些 IP 可以直接访问源服务器

想要限制哪些 IP 可以访问我们的源服务器,我们总不能手动将用户的 IP 地址添加到防火墙白名单,家庭宽带很多时候并不会有固定 IP,每次用户 IP 变动时,你都需要实时将用户的 IP 添加到防火墙白名单,这相当的麻烦,而且你总不能在用户的设备上安装软件实时检测 IP 变化,这个解决方法很不现实

但如果我们使用的是 CDN,那解决方法就很方便了,由于 CDN 会负责转发用户的请求,使用 CDN 后用户与源服务器的请求始终是由 CDN 负责的,服务器日志中只会有来自 CDN IP 的请求,我们只需要使用防火墙拒绝其他 IP 的连接请求,仅允许来自 CDN IP 的请求即可

不过,你需要提前查询一下你使用的 CDN 供应商是否能提供 CDN 全部 IP 的列表,不然下方的两个方法都不适用你的情况

下面部分内容来自 屏蔽 Censys 扫描器, 及设置仅允许 Cloudflare 回源 - Zikin 的独立博客 文章,同样在此感谢 Zikin

我们有两种方式可以对设置仅允许哪些 IP 与源服务器建立连接:

方法 1: 使用 Nginx 的 allow / deny 字段 (不推荐)

由于这是一个不被推荐的方法,我并不会写出具体的操作流程,如果你还是想看看如何配置,你可以去 屏蔽 Censys 扫描器, 及设置仅允许 Cloudflare 回源 - Zikin 的独立博客 文章查看操作步骤

这个方法是可以不向被限制的 IP 发送原本的网页数据,但在搭配了上面添加的 nginx 拒绝握手特性时,会出现一个自欺欺骗人的情况。我们先保留之前的拒绝握手设置,再往普通配置里添加 deny 字段拒绝所有 IP 的连接请求:

# 此处作用为转发 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 <SSL 证书公钥文件>;
  ssl_certificate_key <SSL 证书私钥文件>;
  # 忽略了一些配置

  # 方便测试,这里我们将拒绝来自所有 IP 的连接请求,使用时,你需要改为仅允许来自你 CDN 供应商的 IP
  deny all;

  location / {
    proxy_pass http://localhost:3000;
    # 忽略了一些配置
  }
}

我们再使用 curl 发送请求,域名被替换为 example.com,源站 IP 依然为 333.333.333.333:

我们先试试直接请求 IP

$ 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

与预期相同,服务器拒绝了我们发送的握手请求,也没有提供证书,接下来我们试试强制将域名解析到源站的 IP 地址:

$ 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:
# 忽略了服务器证书内容
# 忽略了请求信息
> GET / HTTP/2
> Host: example.com
> user-agent: curl
> accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
< HTTP/2 403
< server: nginx
< content-type: text/html
< content-length: 153
<
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>
* Connection #0 to host example.com left intact

可以看到,curl 成功与源服务器握手,并输出了来自 nginx 的回应,虽然最后输出的是 403 Forbidden(无权访问) 的回应,也没有输出原本的网页信息。看上去 IP 白名单起效果了,但它并没有达到我们想要的效果,此时我们再试试将一个不存在的域名解析到源站的 IP 地址:

$ 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

从最后的输出可以看出,这个不存在的域名,触发了我们的拒绝握手配置

如果向源服务器发送请求时传递的域名正确,则可成功握手,否则将返回拒绝握手的 tlsv1 unrecognized name 错误

这代表当攻击者使用域名对我们的服务器进行扫描时,直接请求 IP 或附加的是错误的域名,那么你的服务器会正常的拒绝握手。但如果请求的是正确的域名,服务器接收了请求、完成了握手,然后返回了 403 Forbidden(无权访问)

有什么问题呢?将视角换为攻击者就好理解了:我直接请求 IP 你拒绝我的握手,用其他域名请求你也拒绝握手,但我用 example.com 你通过了握手再给我返回 403?那这个 IP 绝对跟 example.com 这个域名有关系

后面的就不用我说了吧,可以等着换下一个 IP 了

方法 2: 使用系统防火墙

在这里,我们使用基于 iptables 易用接口的 ufw 设定防火墙规则,此处部分内容同样来自 屏蔽 Censys 扫描器, 及设置仅允许 Cloudflare 回源 - Zikin 的独立博客 文章

我们照样先试试被防火墙封禁后,直接请求 IP 与携带域名请求会怎么样,先设置防火墙:

sudo ufw allow OpenSSH # 设定防火墙允许 ssh,否则可能就连不上服务器了
sudo ufw default deny # 将默认入站连接设定为拒绝

注意!如果你需要通过 ssh 才能连接到服务器,请务必按照上面的方法允许 ssh 请求,否则你可能会再也无法连接到你的服务器

此时,我们已经设定好了防火墙配置,允许 ssh 连接,并拒绝所有入站请求,接下来我们就可以通过 ufw 开启防火墙了:

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)

这时候再去试试请求服务器,你会发现不管是单独请求 IP 还是携带域名请求都无法获取到内容了,可能会提示无法连接或者迟迟没有完成请求,因为这个时候请求到达防火墙的时候直接被拒绝了,没有返回任何信息

不过这个时候再去浏览器访问域名,你会发现域名也进不去了,因为上面的规则阻止了所有 IP 的入站请求,CDN 的 IP 也同样被阻止了,所以我们还需要设定规则允许来自 CDN IP 的连接

与用户多变的 IP 不同,CDN 的 IP 一般是固定的,我们只需要将 CDN 供应商提供的 CDN IP 列表添加到防火墙白名单即可

我使用的 CDN 来自 Cloudflare,如果你使用的是其他 CDN 提供商,请手动查找对应的 IP 范围列表,按照 前面 的需求,Cloudflare 是提供全部 IP 的列表的,可见 IP 范围 | Cloudflare,我们只需要将列出的 IP 范围添加到防火墙白名单,通过域名访问的请求即可恢复正常:

sudo ufw allow from 173.245.48.0/20
sudo ufw allow from 103.21.244.0/22
# 省略后面一条条加的过程

测试的时候我觉得很麻烦,于是我写了一个 shell 脚本,发布在 GitHub Gists 上,可以一键获取 Cloudflare CDN 的所有 IP 范围列表并添加到防火墙白名单,你可以使用它来一键添加:

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 > add_cf_ips.sh && sudo bash add_cf_ips.sh --remove

对了,在添加完防火墙规则后,你可能需要运行一下 sudo ufw reload 加载修改后的规则以生效,之后再去浏览器使用域名访问,就可以正常访问了。使用 curl 请求 IP 或携带域名请求 IP,都不会返回站点信息,我们就完成了对源服务器的保护

最后的一些琐事

如果设定了防火墙白名单,那么上面 nginx 的拒绝握手配置其实也可有可无了,非 CDN 的 IP 根本无法触发到这个拒绝握手的规则,最坏的情况只能是攻击者用 CDN 的 IP 去扫服务器请求,如果真有这种情况,可以考虑换一个 CDN 提供商了

在想到防火墙添加 Cloudflare CDN 的 IP 白名单时,我还在考虑如果攻击者连上 warp 之后扫服务器怎么办,我就去查了查 warp 的 IP 段,跟 CDN 并不在一个段。这下除非去机房一台机一台机的拔网线,不然基本就不可能找得到我 IP 了

文章末尾感谢:

Cloudflare
DNS / CDN / PaaS 提供商,提供了很多好用的服务,帮我缓解了很多通过 CDN 的攻击,让我的网站不至于关站跑路

Vercel
PaaS 提供商,服务真的非常好用,尽管我的流量被攻击刷到远超 100GB 的 1.2TB 后,没有向我发送账单也没有封禁我的账号

本文提到文章的两位作者
再次感谢您的教程,让我可以学习到需要的网络安全知识

某个服务器提供商
由于安全原因我这里不能说是哪个服务器提供商,但还是在此感谢一下,不然我就没地方放自部署服务了

不知名的攻击者
从 23 年 7 月开始将我服务器打到空路由,在我建立防护后顶着 Cloudflare 一直给我刷了估计有 30m 请求。同时给我 Vercel 的免费 100 GB 套餐超额刷至 1.2TB,让我被迫学习网络安全知识

域名变动

作者 Hubert Chen
2024年2月16日 08:00

个人站点的域名将迎来一些变动,位于 trle5.dev 域名下的全部站点,其绑定的域名将被迁移到 trle5.xyz 域名下

站点迁移后使用的域名:

服务 先前域名 新域名 备注
Hubert's Gitea trle5.dev gitea.trle5.xyz
Hubert's Alist alist.trle5.dev alist.trle5.xyz
Cloudflare tunnle git.trle5.dev git.trle5.xyz

日程

从此文章发布起,当您访问任何一个使用 trle5.dev 域名或子域名的站点时,您都能在网页顶部看到一个提示,并有一个超链接将您引导到这个页面

在 4 月 1 日前,您依然可以照常通过旧域名访问 trle5.dev 下的全部站点,但我不保证功能是否依然正常

4 月 1 日后,位于 trle5.dev 域名及其子域名的站点将被 301 重定向至新域名,直到 6 月 4 日域名到期 DNS 停止解析

trle5.dev 域名过期后,可能会购入重新使用,如果域名被重新注册,您应该能在网页顶部再此看到一个链接到此页面的提示,文中应该也更新了域名的使用信息

new tplate

作者 Hubert Chen
2025年11月20日 08:00

起因

我的 Pixelbook Go 的 Linux 虚拟机在某一天再次(是的,很多次)炸掉了,于是我开始将更多的开发工作挪到台式机上。但是呢,我的台式机配置依然还是 8 GB 内存。

是的,我那台 8 GB 内存的台式机已经没法跑起 Urara 模板了,在构建时会因为内存不够而失败 但这并不是我这么久没有更新的原因

时间间隔太久,我也忘了是什麽时候了,我觉得 @kwaa 写的 lume_theme_shiraha 很合我的胃口,MD3 自适应配色加上样式,非常的喜欢

后面因为对 Pixelbook Go 的逐步厌恶,我反倒去买了台只有 8 GB 内存的笔记本(ThinkPad X1 Carbon Gen 4),它给我的印象还不错,至少比前面那台能干的活多... 本来我想在这里放一个 Pixelbook Go 的链接,因为我真的喜欢它的设计,但是 Google 似乎把它从商店里下架了

但是呢... 我发现在跑 lume_theme_shiraha 时,deno 依然是得吃掉 4 GB 的内存,这是 JavaScript 的原罪吗...

现在

我于是想写一个尽量不用到 JavaScript 的博客生成模板(至少在构建时不能用),同时它还需要支持我的一些奇怪想法,于是就写,如果你能看到这个文章,那我就是在写,或许已经写好一大半了

我打算先支持一下完整的 Markdown 文章解析,再考虑其他的附加东西,最后替换掉正在使用的 Urara 博客,当然还有一大堆事情要做

⚠ 注意:此项目不遵循任何网页设计规范,您可能会遇到以下各种情况

  • 主容器不遵循 768px 最大宽度
  • 可能并不适合人类观看的配色
  • 乱用字重和行高

前期

我选了 go 语言来写这个东西,毕竟我就会两门语言,要是用 C 写的话那就不知道要写多久了(说实话也很久没写了)

其次就是我在使用 Gitea 时发现,它用到了一种叫 .tmpl 的文件,只要去仓库里把对应目录下的 .tmpl 拖到对应的目录,再修改其中的内容,就可以直接更改网页的布局,不需要构建甚至重启 还是要的,但可以做,感觉非常的神奇

但我上手了几次后,发现它的逻辑确实是有一点难懂的,如果去看 Gitea 的模板的话,它也不是按照引用 template 来嵌套组合网页的,而是从头到尾拼出来的,也就是 header + 某些组件 + 实际内容 + footer 这种类似字符串拼接的做法,其中 headerfooter 的标签并不是完全闭合的,当案例去看很误导人怎么用

而且 .tmpl 没有静态检查拓展,语法之类的就问 GPT 去吧

文章解析器

一开始我只使用了 lute 作为 Markdown 解析器,然后我发现它似乎没有什么好的方法可以在将 Markdown 解析成 HTML 的过程中将类或者 ID 选择器加到 HTML 标签中,于是开始邪恶的将 YAML 作为文章源文件(对的,现在还留着,现在这篇文章就是用 YAML 写的 放弃了)

但我尝试写了一会后,发现这样跟手搓 HTML 似乎没什么区别,过了一阵子又改回来了。但样式的问题解决了,CSS 的类型选择器 也不是不能用,而且不用往 HTML 标签里单独塞东西感觉还不错

期间搬了自己几篇文章试试效果,就是不知道为什么不能解析 Markdown 的 Front Matter 部分。没有什么想法,于是跑回去写 bot,由于配置文件我也用的是 YAML,结果越看越眼熟...

这个 Front Matter 部分怎么长得跟 YAML 有点像,而且 Front Matter 前后的 --- 符号似乎也是 YAML 的单文件分隔符,询问神奇的 ChatGPT 后,得到了肯定的答复,于是简单写个解析器区分 YAML 和 Markdown 部分单独解析,文章信息就好了

代码高亮

本来是不打算做代码高亮的,原因很简单,我写不出来

但是呢,lute 支持代码高亮,虽然中途发现样式似乎有奇怪的问题,但后面改了一下,效果还不错,但配色跟我的主题看上去不是很好,现在还没有改的打算

Spoiler / 遮罩

由于 lute 好像不支持 Markdown 的 Spoiler 语法(即 || 遮罩内容 ||),而我之前又有在部分文章中使用到了这个语法。找了一圈 HTML 中也没有对应的标签实现

当然你能在这个文章中看到这个语法,就代表我用了一些奇怪的实现方式 因为我把 <mark> 标签夺舍成 Spoiler 语法了,实际上就是 ==遮罩内容==

这可能会导致在一些 RSS 订阅的阅读器中降低一些阅读体验,因为没找到什么好方法,而且在手机上,必须得长按选中才能看到实际的内容

文章目录和章节

lute 好像没有方法在渲染 Markdown 后拿到一些章节数据,不过还是提供了章节标题锚点的功能,见章节标题右边那个小图标

改了一下,靠 JavaScript 实现了类似 lume_theme_shiraha 的标题链接锚点效果,本来也想写到标题前面,但是因为我设定的 body 填充范围小了些,放到外面在移动端上不是很方便,就顺着放到右边了

一开始是靠作为 lute 标题链接锚点的伪元素实现的,但这样其实还是会留着 svg 元素,标题多了浪费流量,后面问了问 GPT 才给出一个用 JavaScript 代码自动查找元素并添加子元素的方法,但这也导致了如果禁用 JavaScript 那就没法显示出这个按钮

也因为这个,所以文章目录不是很好写,或许可以写一些正则表达式,逐行匹配 heading 标签来尝试生成目录,这个缓一缓也没什么问题了...

我还想设计成每个段落都像代码编辑器里那样可以折叠显示,这样相对来说可能方便些?不过感觉很难做

样式

由于 前面说到 lume_theme_shiraha 很合我的胃口,就打算照着样子做一个(不过就不是最新版那个样子了),还有部分设计借鉴了 Urara,非常感谢 kwaa

当然前者是有 MD3 按图片取色的,这个我尝试从网上找一个 MD3 取色的 go 语言库,完全没找到于是放弃,就固定两个配色就好了...

个人偏好

期间还掺杂了个人偏好的样式,例如我觉得全屏看网页时 768px 的主容器宽度未必显得两侧太空了,于是在窗口大小大于 1536px 时,主容器宽度会拓展到 1024px。也不至于出现 21:9 的屏幕读一行字需要头从左往右转的情况

由于没有使用 css 库之类的,样式全都是自己写的,还可以把样式全部写进 HTML 的 <head> 标签里,这样只需要按 Ctrl + S 就能按照原样保存的网页

不过文章里有引用网络资源的话就还不行,或许能想个办法在构建静态网页时把引用的网络资源都一起扔进 HTML 里。但这样可能会因为重复加载相同的样式和资源而消耗多余的流量甚至拖慢加载速度,这个就之后再说了

按需样式加载

好像前端那些样式库之类的东西,都会根据使用到的样式来按需构建样式表文件,而我思考了一下,好像做起来很难,但每个页面都加载用到的全部样式又比较浪费流量,思考了一段时间后我选择了另一个方法

把全部的样式文件,看哪些组件会用到哪些样式,将其分别存放到对应文件夹中,在构建时存储到变量中,当调用这个组件时,自动在组件前附加样式

本来还想把样式都丢在 <head> 标签里,但是在 .tmpl 模板中进行操作时无法更改外部的值,好在测试都正常,就暂时用吧

文章列表

想到之前使用 Urara 时会将题图放到文章列表中,于是自己试了试也做一个,发现效果很难看,甚至想过要不还是不显示图片了

后面又乱折腾了一阵,最后是作为文章列表项目的背景图像显示的,并叠加了一个背景色到透明的渐变来模拟遮罩效果,在深色模式下观感可能还可以,但浅色模式下可能就不太行了

而且图片的显示区域是由设定文章描述高度来撑开的,想不到什么办法了...

通过 PageSpeed Insights 测试了一下,这个题图实现方式还有个应该应用 fetchpriority="high" 的建议,但我这个是 <div> 标签的 background 样式,添加属性也没效果,就暂时先这样了

文章分类和标签

在之前使用 Urara 的时候是有从其他仓库里拿过一个 sections 插件(见 Interstellar750/hexo_s 的 README 部分),可以以某个分类来显示其下的文章,这个代码在我的 Urara 库中还有保留,但是 @jiwaszki 似乎转向使用 hugo 来搭建博客,原来的代码就不得而知了...

好在这个写起来也不算太难,其实仔细想想跟 tag 也没差多少,就是分类只能指定一个,而标签能指定多个,最终都是靠标签或分类来索引符合条件的文章而已

由于它们很像,索引过程也都是用的文章列表模板,我把它们都放到同一个页面去了,然后再遍历生成 HTML,分类的的 URI 就是 host/category/<分类名>,标签就是 host/category/_<标签名> (前面多一个下划线)

在分类展示页里,分类以 grid 的形式显示在前面,显示分类名、最后发布的文章时间和其下的文章数量。后面部分就留给标签,如果有某个标签包含多个文章,也会显示计数

不过相比 Urara 就不能按分类 + 标签逐个筛选了,只能一个个看

网页交互

RSS 订阅 / Sitemap 数据

一开始我觉得 HTML 也是一种 XML 嘛,就直接当做 .tmpl 文件,进行一个模版的写,但是由于开头的 <?xml version='1.0' encoding='utf-8'?> 不是一个正常的 HTML 标签,就会导致被转义掉,就算用 template.HTML() 套住也不行,它在执行模板时就被转义了,没法被正常识别成 XML 文件

中途想了各种奇怪方法来解决这个问题,最后在写文章的时候突发奇想:

既然就开头那一行有问题,那我先把它去掉,执行完模板后再把开头作为字符串把它拼起来不就好了

于是现在就只需要在使用时填一下 "atom" 参数就好了,舒服

途中还看了看 AtomRSS 的区别,我一开始还以为 RSS 是 Atom 的升级,没想到是反着来的

后面顺带把 sitemap 也做了

链接预览

在写了一半发出去让群友们不得不品鉴时,发现好像没有链接预览,试了试几个博客链接,最后发现是 <meta> 标签的问题

途中看了各种网站,最后的结论大概是得加 <meta property="og:description" content="描述"> 这种标签,但是总感觉不太对劲,我在 MDN 甚至都没看到 meta 标签有 property 这个属性...

还尝试过为文章生成链接预览图片,但尝试了一会后发现设计出来的图相当难看... 暂时搁置了

评论系统

好在我使用 Urara 时并没有添加太多的评论系统,只用到了 GiscusWebmention,这让我的迁移过程变得比较轻松

Giscus 本来就提供 JavaScript 代码的使用方式,而 Webmention 显示嘛,用 kwaa 写的 seia 就好了

提交 Webmention 链接的话,去了解了一下 API 端点,请 ChatGPT 写了一份代码,就好了

隐私声明

作者 Hubert Chen
2025年10月24日 08:00

本站源代码存放在 GitHub 仓库,博客托管在 Cloudflare Pages 平台,域名 DNS 解析由 Cloudflare 提供,我可得知的数据如下:

GitHub 仓库

  • stars / watching / fork 的用户列表

  • Discussions 的讨论内容(包括 Giscus)

  • Pull Request 请求信息

Cloudflare / Cloudflare Pages 平台

流量分析

Cloudflare 为域名托管者提供了免费的流量分析,最长可以记录 30 天的数据,借此,我可以得知的数据如下:

此功能为使用 Cloudflare 域名托管服务时默认开启的功能,您可能无法通过任何方式绕过这部分的数据收集

Web 流量
  • 请求(最长 30 天,最短 24 小时,精确到请求数)
    • 请求总数
    • 已缓存的请求
    • 未缓存的请求
  • 带宽(最长 30 天,最短 24 小时,精确到 KB)
    • 总带宽
    • 已缓存的带宽
    • 未缓存的带宽
  • 唯一访问者(精确到人数)
    • 总计(最长 30 天,最短 24 小时)
    • 上限(最长 1 天,最短 1 小时)
    • 下限(最长 1 天,最短 1 小时)
Web 流量请求
  • 访客流量靠前的国家或地区(精确到流量数,最长 30 天,最短 24 小时)
  • 统计信息(上个月)
    • 已节省的字节数(精确到 KB)
    • 已提供的 SSL 请求数(精确到次数)
    • 已阻止的攻击数(精确到次数)

Web Analytics

Cloudflare Web Analytics 是一个基于 JavaScript 代码的分析工具,最长可以记录 180 天的数据

您可以尝试使用一些工具来屏蔽 https://static.cloudflareinsights.com/beacon.min.js JavaScript 代码来绕过此部分的数据收集。您也可以选择对于本站点(trle5.xyz)屏蔽 JavaScript 代码,但这可能会导致一些功能将无法使用

在控制台中,可以按最长 30 天,最短 1 分钟的时间间隔来查看数据,同时还可以使用 国家/地区 主机 路径 引用方 设备类型 浏览器 操作系统 站点 排除自动程序 来作为筛选条件,借此,我可以得知的数据如下:

高级指标
  • 页面加载时间(精确到毫秒)
  • 访问量(精确到次数)
  • 页面浏览量(精确到次数)
  • 核心网络指标(Core Web Vitals)见下方
核心网络指标(Core Web Vitals)
  • 最大内容绘制(LCP)(精确到次数)
  • 与下一个画图交互(INP)(精确到次数)
  • 累计布局偏移(CLS)(精确到次数)

以上三个指标数据,均可按照 URL 浏览器 操作系统 国家/地区 元素 类别来排序

在控制台中还提供了调式视图,在调式视图中可以查看被记为 需要改进 元素的详情信息,其中也可查看到一些数据:

最大内容绘制(LCP)
  • Object 和 页面
    • 主机
    • 路径
  • 指标延迟
    • LCP - P50
    • LCP - P75
    • LCP - P90
    • LCP - P99
  • DOM
    • 元素
与下一个画图交互(INP)
  • 页面
    • 主机
    • 路径
  • 指标
    • INP - P50
    • INP - P75
    • INP - P90
    • INP - P99
  • DOM
    • 元素
累计布局偏移(CLS)
  • 页面
    • 主机
    • 路径
  • 指标
    • CLS - P50
    • CLS - P75
    • CLS - P90
    • CLS - P99
  • DOM
    • 元素
  • 布局偏移
    • 上一个
    • 当前

关于统计数据

统计数据中绝大部分可展示的数据已列举于上方,但此列表可能依然会有遗漏,若有其他需要了解的数据请前往对应平台中查看相应文档

您在本站的浏览统计数据可能会被公开展示,点此查看公开展示时的类似形式
  • 与他人展示
    • 你看看我这篇文章竟然有这么多人看:(附上页面浏览数据)
  • 演示流量消耗
    • 压缩了图片大小后,我博客每个月只需要不到 1GB 流量:(附上流量消耗统计)
  • 浏览器类型
    • 访问我博客用的浏览器七八成都是 Chrome:(附上浏览器类型统计)

待办事项

作者 Hubert Chen
2026年3月8日 08:00

嗨,你好!

顾名思义,这是一个待办清单,我会把我的博客更新计划和完成进度记录在这里,不过也不是全部列在这里的事情都会完成,有时候会遭受到不可抗力的影响

博客相关

文章更新

  • 写一篇关于 go 错误处理相关的文章
  • 还没有想法...

需要修改 / 补充的文章

  • 还没有想法...

其他

Link Preview 时的图片
  • 搓了一些,但难看到自己都不想看...

建站历程

作者 Hubert Chen
2025年10月24日 08:00

想了想以后这个页面似乎会长长长,就单独放个页面了

以后也得想个办法整理一下这个页面

建站初期 & hexo & chic

2022/01/24 初次建立
初次建立并使用 GitHub Pages 来作为服务器 (其实并不是第一次,前前后后试了好几次,因为碰到了好多 bug)

2022/01/29 白嫖域名
Freenom 上申请了 12 个月的免费域名 trle5.tk ,但由于不会设置 DNS 解析,依然用着 GitHub Pages 的默认域名

2022/02/26 迁移后端
由于 x10m2 上的 Sailfish OS 因为未知问题操作很卡,把 hexo 后端备份出来迁移到了 s10e 上,使用 Termux 来继续运维

2022/02/27 设置 DNS 解析
将域名 DNS 解析托管到 GoDaddy ,成功用上了自定义域名

2022/03/08 购入新域名
reg.ru 购买了一年的 trle5.xyz 域名,用于存放文件,本站可能有一些图片文件会储存在里面

2022/03/14 更换 DNS 解析
更换为由 Freenom 提供的 DNS 解析

2022/03/15 更换存文件的地方
把 blackbox 的域名从 trle5.xyz 迁移到 t5d.trle5.tk

2022/05/07 换 Chic 主题
从默认的 Landscape 主题更换为 Siricee 制作的 Chic 主题

2022/05/08 建立镜像站
使用 Vercel 建立了博客镜像站,使用域名 trle5.xyz ,DNS 解析由 Cloudflare 提供

2022/06/01 重新配置主题
重新配置了博客主题,因为 05/15 那天手机数据丢失让博客后端也部分丢失了 (真的丢了好多东西啊😢)

2022/07/19 修正文章
重新修正了一篇文章,之前写的文章发现达不到所想的方案所以就暂搁了 ,由于之前的丢数据问题,Cloudflare 账号也登不上了,找回有点麻烦

2022/09/11 成功找回 Cloudflare 帐号
给 Cloudflare 客服发邮件,成功拿回了账号的控制权,目前 trle5.xyz 站点会随时更新,就是代表上面可能会有一些没写完的文章和新东西,文章写完后再推送至 trle5.tk,于 6 月 4 日在 Porkbun 白嫖的 trle5.dev 域名还没打算好用来干嘛,两个付费域名以我现在的经济能力压力有点大

切换至 Urara

2022/10/23 切换到 Urara 后端
花了差不多整个下午的时间一直搞到凌晨两点多,终于把 Urara 后端配置好了,从九月月底在 Dejavu's Blog 频道看见 Urara 直到今天才完全成功切换到这个后端,配置其实不算太难,我完全没有 svelte 的基础,但看着原文件和 Urara 作者 藍+85CD 的博客改也勉强可以了,后面估计我还得改不少东西

2022/11/02 小修小补
还是没能摸透 Urara 是怎么要求文章的,好像对 # 号有要求,对文章重新调整了,测试过大概都正常了

2022/11/13 移除了加都没加的东西
从 8 号开始搞到现在,搞好了一个朋友页面,但很奇怪在本地运行一切正常,部署到 Vercel 上就无法运行,用之前的部署方式会出现 404 问题,于是现在只能删掉了 😭

修好咯,不过目前有个鼠标放到文章日期上时修改时间会与创建时间叠在一起,看看 kwaa 大佬什么时候修

顺便说一句,trle5.dev 域名借给朋友拿去搭博客了

2022/11/19 同步与开源
昨天晚上给 Urara 的 这个 议题发了一个 comment,今天中午问题就修复了,要在这里说一句非常感谢大佬们,自己没学过前端也就只能提个议题或报告一下问题了,修复问题这种问题很可惜帮不上忙 😥

推完这次更新后我的整个博客的源码就开源啦,至于之前为什么不放出来,第一个原因是可以用之前的 blog 项目直接部署,但换到 Urara 后端之后这样就不行了,除非点击任意链接后网页链接都会自动加上 .html 后缀,不然就会出错,没那个技术力的我就只能弃用之前的方法了,第二个就是当时有巨多问题,自己修也不知道怎么修

2022/12/01 开通 Giscus
成功开通了 Giscus 讨论功能,比想象中的要简单好多,不过看 #47 这个议题好像说会有 bug 导致登不上 Giscus,但我自己试了试没问题,有点奇怪

修了,但似乎只修了 Giscus 的登录问题,sitemap 还是没法使用

一周年

2023/01/24 续费域名
博客也一周年了,先要续费的是在 freenom 上注册的免费域名 trle5.tk,不出意料的免费续费了一年,虽然目前已经闲置很久了

博客目前主要用的 trle5.xyz 打算在今天转移到 Blacknight,本来是打算转到 Cloudflare,但架不住 5 欧元一年 的续费/转移价格,便宜

但是被限制付款了,发个工单问问客服,不行那就高价转到 Cloudflare

至于 trle5.dev 最便宜还是 $10 以上,但过期时间在六月,再等等吧

2023/03/02 购入服务器
RackNerd 买了个服务器,折腾两天后搭好了一个能用的 memos,分配的二级域名为 memos.trle5.xyz

还有一个 SSL 证书时不时抽风的 Gitea 服务则是为了操作方便把 trle5.dev 拿来用,原来朋友的博客就给他安排了个二级域名:blog.trle5.dev,但他好像打算把博客重构了

2023/07 被 DDoS
被打我是莫名奇妙,服务器我只搭了些自部署的服务,搭在 Vercel 上的博客和下载站点也被 DDoS 刷掉大把流量。好在整体没什么影响和损失,只花了个换 IP 的钱,还学了不少防 IP 泄露的知识。在此感谢 Cloudflare 的防御和 CDN 🙏

新的博客后端

2025/08 计划开始
在了解 .tmpl 文件的一些简单原理后,打算想写一个新的博客生成工具,虽然计划暂搁了一会,但还是在九月中期继续折腾,设计大部分仿照着 lume_theme_shiraha 做,小部分设计参考之前使用的 Urara

2025/10 接近可用状态
在一阵子一手 GPT 一手 Google 的情况下,已经解决了大部分看上去有问题的样式,尽管代码部分还不怎么样,但至少能用了,更多内容详见 new tplate 文章

关于我

作者 Hubert Chen
2026年3月8日 08:00

大家好啊,我是 Hubert,今天来点大家想看的东西啊

会些什么

Go 语言 现在写了几个小项目,自我认为还可以

C 语言 说实话已经很久没写过了

Linux 没什么变化,会些日常用的命令就足够了

兴趣爱好

日常消遣方式是在 Telegram 上聊天与刷频道,其次就是玩游戏

有时会画点画画,但还没到入门的阶段

联系方式

依然是没有国内的交流平台,不是很喜欢用,有联系需求就发邮件吧

Telegram: @trle5 / @include011

Matrix: @trle5:matrix.org 不怎么活跃,但应该能收到消息

Email: 01@trle5.xyz 用的 Cloudflare 转发到 Gmail

Twitter: @interstellar750 不是很喜欢发推文

Fediverse: trle5@hyp3r.linkkwaa 大佬的实例上注册的,也是不怎么喜欢发文

GitHub: @interstellar750 (好像并不能联系到人...)

Gitea: @trle5 自己在 VPS 上用 docker 跑了个 Gitea,已经运行三年了

GPG 公钥

AAC3 7641 0634 FF86
sec   rsa4096/AAC376410634FF86 2023-01-01 [SC] (签名 证明)
      7456 A0AB 47EC E8BE 1AD0 89D9 AAC3 7641 0634 FF86
uid   [ultimate]    Hubert Chen <01@trle5.xyz>
ssb   rsa4096/B716CE1EAA7B8F00 2023-01-01 [E] (加密)
ssb   rsa4096/B4ED58260C725C91 2023-01-06 [A] (认证)
ssb   rsa4096/2935B4DE0D6F7720 2023-01-06 [SE] (签名 加密)
8A2A 227E 222D CCDB
sec#  ed25519/8A2A227E222DCCDB 2023-01-18 [C] (证明)
      F154 5A09 2296 673A 0C43 6BE0 8A2A 227E 222D CCDB
uid   [ultimate]    Hubert Chen <01@trle5.xyz>
ssb>  ed25519/74D8BCE883FDDEE2 2023-01-19 [S] (签名)
ssb>  cv25519/FA47AF4129AA0BB1 2023-01-19 [E] (加密)
ssb>  ed25519/7043720D3C7D7718 2023-01-19 [A] (认证)

当你需要确认屏幕前的人为“我”时,你不需要任何理由即可向我索要以上方 GPG 密钥进行签名的消息,收到签名的消息时请务必验证消息的时效性,同时请确保验证消息中包含双方的信息以及需要的操作内容

若验证失败或以任何方式拒绝或推脱签名请求,请不要信任此时“我”的任何请求!

获取公钥文件: GitHub | Hubert's Gitea | Hubert's Box

目前主要用来给 Commit 签名的密钥为 2935 B4DE 0D6F 7720,暂未对其他 GPG 公钥进行证明

游戏平台

Steam: interstellar
Xbox: interstellar771
Ubisoft: interstellar750
Minecraft ID: trle5
EA ID: trle5

音乐

Spotify: Hubert Chen
网易云音乐: trle5 不是很常用

目前主要是用 Spotify,喜欢听的音乐类型比较杂

设备

移动设备

iPhone 12 A14 / 4GB / 128GB
iPhone SE 2 A13 / 3GB / 64GB 坏掉了...
iPad mini 5 A12 / 3GB / 64GB
Apple Watch S5 44mm / 32GB

硬件

i5-4460 / HD 4600 & Tesla P4 / 8GB DDR3 / 240GB SATA SSD x 3
Orange Pi Zero2 H616 / 1GB DDR3 / 64GB Micro SD
Xbox Series S 1TB

操作系统

Windows LTSC 2021 (21H2)
Debian trixie | kernel 6.12 服务器用
iOS 15.6 (iPhone 12) 17.7 (iPhone SE 2)
iPad OS 17.3.1 (iPad mini 5)

关于本站

想想也是得在这个页面写一点关于本站的信息

博客后端

建立初期使用 Hexo,使用过 Chic 主题,很长一段时间在使用 Urara 模板,现在用的是自己写的 tplate

评论系统

目前支持基于 GitHub DiscussionsGiscus 以及基于 webmention.ioWebmention,由于我换成了自己的博客模板,显示信息的部分就交给 kwaa 开发的 seia 插件了

为 Chrome OS 设置代理

作者 Hubert Chen
2024年8月6日 08:00

⚠ 文章未完成

Chrome OS 因为安全原因逐步隔离了用户空间和 crosh 终端

这导致了原本固定映射至用户下载文件夹的 /home/chronos/Downloads 目录已经消失了

于是现在找不到一个稳定的目录可以直接将 sing-box 的二进制文件转移到 crosh 中的 /usr/local/bin/ 目录中(可能有,但我心灰意冷懒得找了)

而如果在 crosh 中直接使用 curl 从 GitHub 上下载文件,大概率会因为 DNS 污染等原因下载失败,没有一个好用的办法...

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

点击来帮助 Chrome OS 夺回属于它的内存

在某一天调式程序时,我发现请求的 API 似乎迟迟没有响应,想着是不是节点出问题了?当我尝试打开 Android 上的代理软件时,我意识到并不是网络问题,而是这个 Android 虚拟机,在只跑了一个代理软件的情况下,卡死了...

保存代码进度再重启设备后,Android 虚拟机正常了,继续写代码调程序,但那天晚上我意识到,我似乎启用这个 Android 虚拟机,为的只是跑一个代理软件?于是在第二天我备份好 Android 虚拟机中的必要数据后就把它删掉了。然后我在诊断程序里看到设备的空闲内存从日常的 2GB 变为了... 12GB

剩下的就不多说了,现在 Chrome OS 夺回了属于它的内存,只需要解决网络代理问题就可以了

如果你的网络情况还不能在脱离了 Android 环境中的代理软件下访问网络,那请先不要着急删掉它

删掉了 Android 容器后,就得考虑选择替代的代理方式了,目前大概有四类设置代理的方式,在下方表格中按配置难易度从低到高排序,还有能代理到的范围和优缺点

代理类型 Chrome crostini crosh 优点 缺点
Wi-Fi 代理 简单方便,可连本机或其他设备 代理范围过小,Linux 虚拟机吃不上,每个 Wi-Fi 都需要单独配置
WireGuard 代理范围广,有系统 UI 控件可快捷切换 需要进入 crosh 添加配置,协议因素有速度劣势
Tun 代理范围广 需要在 crosh 中配置代理软件,可能还会有些奇怪问题
TProxy 暂时想不到 代理范围一般,Linux 虚拟机会直接没网(不只是没代理)

非侵入式(不修改 Chrome OS)

我们有两种不需要安装任何软件就能添加代理的方式,它们就是 Chrome OS 自带的 WireGuard 和比较差劲的 Wi-Fi 代理

1. WireGuard

通过 WireGuard 设置代理可以覆盖到浏览器、Linux 虚拟机和 crosh,还有系统 UI 控件,方便又好用。

但是系统自带的图形化 WireGuard 配置添加工具有 bug,必须要在 crosh 中通过命令来添加配置,这代表你必须要清除所有数据,开启开发者模式后才能添加 WireGuard 配置 似乎并不需要,请手动试试能不能通过下方的快捷键打开 crosh,如果可以的话就不需要

此教程来自 ChromeOS Flex: Can’t save Wireguard config (FIX) – Tech Blog (Web Archive link)

由于 Cloudflare 的 Warp 现在被 ban 的厉害,比较推荐自建服务端

首先确保你已经为你的 Chrome OS 设备启用了开发者模式,之后按 Ctrl + Alt + T 来打开 crosh,再逐行运行以下命令,注意根据你的 WireGuard 配置替换掉对应的内容:

wireguard new <名称>
wireguard set <名称> local-ip 10.8.0.9
wireguard set <名称> private-key # 运行这行命令后粘贴密钥,回车保存
wireguard set <名称> peer <公钥> preshared-key # 同上,但如果没有预共享密钥则不用运行此行
wireguard set <名称> peer <公钥> allowed-ips 0.0.0.0/0
wireguard set <名称> peer <公钥> endpoint <端点 IP:端口>
wireguard set <名称> peer <公钥> persistent-keepalive 0
wireguard show # 查看添加完成的配置

添加完配置后,你应该会得到类似以下的输出:

name: wireguard-profile
  local ip: 10.8.0.9
  public key: iJbPYu1dCc1VVU7o6OS5uWhnni9iXCi1nuGkwdsIPn0=
  private key: (hidden)
  name servers: 8.8.8.8, 8.8.4.4

  peer: QiTfo+u2brfoTh5BHQKbU/Rt/OJ7MAQMO0+pMEYNxRg=
    preshared key: (hidden or not set)
    endpoint: 127.1:5300
    allowed ips: 0.0.0.0/0
    persistent keepalive: 0

其中的 public key、peer 以及 endpoint 可能会与上方显示的不同,因为这几个参数取决于你的 WireGuard 配置,然后你就可以到设置的网络板块里启用刚刚添加的配置,试试能不能正常代理流量了

2. Wi-Fi 代理

Wi-Fi 代理更适合只用浏览器上网的用户,因为代理不到 Linux 虚拟机,可以应急用用,长期使用还是推荐选择其他方式

Wi-Fi 代理配置方法不算难,只需要在设置中依次点击 网络 > Wi-Fi,找到目前已连接的 Wi-Fi 名称并点击它就可以查看网络详情信息,再点击最下方的 代理 选项,把连接选项改为 手动配置代理,就可以配置 Wi-Fi 代理了

接下来就是填入 HTTP/SSOCKS 代理主机的 IP 以及端口了,如果你的代理软件支持在一个端口上使用多个协议,还可以把 对所有协议使用同一代理 打开,那样只需要填一个主机的 IP 和端口就足够了,至于下面的 不要对以下主机和域使用代理设置 直接留空,如果你知道这方面的知识,就看你想怎么来了,填完信息记得点击保存

只要你填入的 IP 和端口正确,且对应的设备上运行着负责代理流量的服务,那么这个时候 Wi-Fi 代理就已经生效了,去测试一下吧

侵入式(在 Chrome OS 中安装代理软件)

如果要像 Linux 发行版那样在 Chrome OS 里安装代理软件,那么首先你得给你的 Chrome OS 设备启用开发者模式

注意不是修改 设置 > 关于系统 > 版本 里的开发者版本

具体怎么启用开发者模式我这里就不写了,只讲启用开发者模式后如何将代理软件安装到 Chrome OS 中

如果你选择安装侵入式的代理软件,你就可以选择多种方式来将 Chrome OS 连接到代理,这里首要推荐的是支持 Tun 模式的 sing-box

sing-box

折腾了许久,在 Chrome OS 中似乎只有 sing-box 能使用 Tun 模式来代理设备的全部流量,根本原因大概有两个:

  1. 似乎 Chrome OS 的系统网络栈不支持执行 L3 到 L4 转换,只有在 sing-box 入站(inbounds) 配置中将 Tun 的 stack 参数手动设定为 gVisor 才可以正常转发流量
  2. 同时需要将 sing-box 路由(route) 配置中的 default_interface 参数设定为 wlan0,这个值对应的是给机器提供网络的网卡设备,我的 Pixelbook Go 没有网线接口,我也没有能插网线的拓展坞,没法测试连接网线后是否需要修改。有能力的话请自己到 crosh 里运行 ifconfig 命令判断是否要修改

下方是一份针对 Chrome OS 的 sing-box 配置,配置好了 Tun TProxy 和 HTTP 代理入站,Yacd Web UI 控制台,DNS 和去广告规则集,使用时请手动替换掉出站(outbounds) 中的示例节点:

点击查看配置
{
  "log": { "level": "info", "timestamp": false },
  "experimental": {
    "clash_api": { "default_mode": "rule", "external_controller": "127.0.0.1:9090",
      "external_ui": "ui", "secret": "", "external_ui_download_detour": "",
      "external_ui_download_url": "https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip"
  },
  "cache_file": { "enabled": true, "store_fakeip": false } },
  "dns": {
    "servers": [
      { "tag": "proxyDns", "detour": "proxy", "address": "https://1.1.1.1/dns-query" },
      { "tag": "localDns", "detour": "direct", "address": "tls://120.53.53.53" },
      { "tag": "block", "address": "rcode://success" }
    ],
    "rules": [
      { "rule_set": "geosite-category-ads-all", "server": "block" },
      { "outbound": "any", "server": "localDns", "disable_cache": true },
      { "clash_mode": "direct", "server": "localDns" },
      { "clash_mode": "global", "server": "proxyDns" }
    ],
    "strategy": "prefer_ipv4"
  },
  "inbounds": [
    { "type": "tun", "stack": "gvisor",
      "auto_route": true, "strict_route": true,
      "domain_strategy": "prefer_ipv4", "mtu": 9000,
      "inet4_address": "172.114.0.1/30", "inet6_address": "2001::1/64",
      "sniff": true, "sniff_override_destination": true,
      "inet4_route_exclude_address": [ "0.0.0.0/8", "10.0.0.0/8", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.168.0.0/16", "224.0.0.0/4", "240.0.0.0/4" ]
    },
    { "type": "tproxy", "tag": "TPROXY-IN", "listen": "0.0.0.0", "listen_port": 7895, "sniff": true, "sniff_override_destination": true },
    { "type": "mixed", "listen": "127.0.0.1", "listen_port": 2080, "sniff": true, "users": [] }
  ],
  "outbounds": [
    { "tag":"proxy", "type":"selector", "outbounds":[ "auto", "direct", "outbound_hy2" ] },
    { "tag":"auto", "type":"urltest", "url": "http://www.gstatic.com/generate_204", "interval": "10m", "tolerance": 50, "outbounds":[ "outbound_hy2" ] },
    {
      "type": "hysteria2", "tag": "outbound_hy2",
      "server": "114.51.4.191", "server_port": 9810,
      "password": "example_password",
      "tls": { "enabled": true, "server_name": "exmaple.com", "insecure": true }
    },
    { "type": "direct", "tag": "direct" },
    { "type": "dns", "tag": "dns-out" },
    { "type": "block", "tag": "block" }
  ],
  "route": {
    "default_interface": "wlan0",
    "final": "proxy",
    "rules": [
      { "protocol": "dns", "outbound": "dns-out" },
      { "rule_set": "geosite-category-ads-all", "outbound": "block" },
      { "clash_mode": "direct", "outbound": "direct" },
      { "clash_mode": "global", "outbound": "proxy" },
      { "domain": [ "clash.razord.top", "yacd.metacubex.one", "yacd.haishan.me", "d.metacubex.one" ], "outbound": "direct" }
    ],
    "rule_set": [
      { "tag": "geosite-category-ads-all", "type": "remote", "format": "binary", "download_detour": "direct",
        "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/category-ads-all.srs"
      }
    ]
  }
}

配置文件有了,你可以决定是否要继续操作,接下来的步骤如果操作不当可能会炸掉你的系统

首先判断一下你的 Chrome OS 设备的架构,按 Ctrl + Alt + T 打开 crosh,使用 uname -m 命令来查看硬件架构:

crosh> uname -m
x86_64

由于我的 Pixelbook Go 使用的是英特尔(Intel) 的处理器,所以这里显示的是 x86_64,如果是 AMD(超威半导体) 的处理器,那应该也是 x86_64,也有可能是 amd64

如果你的 Chromebook 使用了高通(Qualcomm) 或者联发科(MediaTek) 的处理器,那可能会显示 arm64 或者 aarch64

接着前往 SagerNet/sing-box 的最新发布页面,根据你的设备架构来拿到最新的 sing-box 的下载链接

如果设备架构是 x86_64amd64,那就选 sing-box-<版本号>-linux-amd64.tar.gz

如果设备架构是 aarch64arm64,那就选 sing-box-<版本号>-linux-arm64.tar.gz

建议打开网站后按 Ctrl + F 进行搜索,先搜 linux-,然后根据你的设备架构接着输入 amd64arm64,最后唯一高亮的那个就是你需要的,右键那一行字,再点击复制链接地址

注意是点击 复制链接地址,不是 复制,否则你复制到的链接地址是错误的

接下来按下按 Ctrl + Alt + T 打开 crosh,逐行输入以下命令并回车,将其作为一个命令脚本保存到文件中

shell
cd /tmp
# 这篇文章完成时,最新的版本是 1.11.7
echo 'curl "https://github.com/SagerNet/sing-box/releases/download/v1.11.7/sing-box-1.11.7-linux-amd64.tar.gz" > sing-box.tar.gz' > download_sing-box.sh
sh download_sing-box.sh && tar xzf sing-box.tar.gz &&

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

作者 Hubert Chen
2024年6月20日 21:22

首先按 Ctrl + Alt + F2 打开 VT-2 终端,输入 chronos 用户进行登录,默认无密码

# 分别创建 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 >> ~/.ssh/authorized_keys
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys

接下来,往 ~/.bashrc 中预设几个命令,好让使用的过程更方便:

echo "alias vt2port=\"sudo /usr/sbin/sshd -p 6969\"" >> ~/.bashrc
echo "alias vt2connect=\"ssh 127.1 -p 6969\"" >> ~/.bashrc

# 使命令生效
source ~/.bashrc

到这里具体的配置就完成了,在 VT-2 中输入 vt2port 后,会自动开启 sshd 服务器,之后你就可以按 Ctrl + D 注销 VT-2 了,除非你重启 Chrome OS 或手动杀掉它,否则它都会一直运行

然后按 Ctrl + Alt + F1 回到 Chrome OS 中,再按 Ctrl + Alt + T 打开 crosh 后,输入 shell 进入终端再输入 vt2connect,会询问你要不要信任主机,确认后就可以方便的在 crosh 中使用 sudo 命令了

以后每次重新启动之后,开启 sshd 服务器需要的步骤就是:

  1. 进入 VT-2 登录 chronos 用户
  2. 输入 vt2port 开启 sshd 服务器
  3. 注销 VT-2 返回 Chrome OS

需要在 crosh 中使用 sudo 权限的步骤:

  1. 打开 crosh > 输入 shell 进入终端
  2. 输入 vt2connect 连接到 sshd 服务器
  3. 运行任何需要 sudo 的命令

参考链接:oddbyte/howto-use-sudo-in-crosh

隐藏站点的源服务器 IP

作者 Hubert Chen
2024年4月5日 08:00

从不开 CDN,域名 DNS 直接解析到 IP,再到服务器被打到空路由后的总结

为什么要隐藏源服务器 IP

首先我们要看看浏览器访问一个站点时,这个请求会如何到达源服务器:

  1. 浏览器获得用户输入的域名,首先向 DNS 服务器发送查询请求
  2. DNS 服务器收到了请求,解析到了域名对应的 IP,返回给浏览器
  3. 浏览器得知了源服务器的 IP 地址,接着继续向源服务器发送请求

DNS 的作用就不多说了,简单来看就是放域名进去,就可以吐出域名对应的 IP

这个过程中,浏览器就已经知道源服务器的 IP 了,怎么知道的呢?当然是 DNS 告诉它的

但 DNS 是怎么知道源服务器的 IP 呢?不是什么魔法,是站点主人自己告诉 DNS 的,不然没了 DNS,想访问站点只能记一串 IP,很不方便

现在我看某个人不爽,想把他的网站做掉

当然我不提倡任何网络攻击,这里只是做一个例子

上面我们知道了请求是如何到达源站的,最简单的方法,就是一秒给他发几千个请求甚至更多,就能起效果。于是立刻手搓了一段代码,一秒发五千个请求,这个叫分布式拒绝服务(DDoS)攻击

由于服务器的配置并不是很高,处理不来我们发送的这么多请求,一段时候后,服务器的主人发现了不对劲,赶紧把 DNS 解析删除了,打算等我们不打他之后再重新添加 DNS 解析,让网站恢复访问

此时,虽然 DNS 解析被删除了,但服主在被打之前在 DNS 里设置的 IP 已经被我们请求过了,我们早已知道了源站 IP。于是我们乘胜追击,把发送请求的域名换成 IP,接着打:

在我们的不懈努力下,服务器的占用爆满,服主只能关机跑路
就算服主重新开机,迎接他的依然是 100% 占用,跑不掉

到这里我们已经知道 DNS 与 IP 公开绑定的后果了,就算站点不能通过域名访问了,只要服务器不关机,那还是能通过 IP 访问的,不做些什么,站点就只能停了

所以,我们要在确保通过域名可以正常访问的情况下,保证源服务器的 IP 不被泄露

有哪些办法可以隐藏源服务器 IP?

我们先代入到前面的攻击事件中服主的视角,看看有什么方法可恢复站点访问,还能让他打不到我们

换 IP

一台机器想要作为站点的服务器,大部分情况下都需要被分配一个可被外部访问的 IP 地址,但它是可以更换的,可能会需要一些成本

更换了 IP 后,我们可以的服务器就可以开机了,因为攻击者并不知道我们服务器新的 IP 是多少,在不设定 DNS 解析的情况下,服务器是暂时安全了。但如果再次把域名解析到新的 IP,那他还是跟前面一样,只需要访问一次就能知道我们的新 IP,服务器就会继续挨打

我们也可以更换 IP 的同时更换一个域名,不过这样可能会损失之前积累的用户。如果你将更换域名的消息告诉了之前的用户,而攻击者潜伏在用户之中,那么你的服务器可能会再次遭殃

若网站之前有被搜索引擎收录,更换后的新域名再次被收录,那攻击者可以通过搜索引擎得知更换后的域名,DNS 再返回对应的 IP,那服务器同样会遭殃

上高防 / 升级服务器

只要你的配置够高,他就没法把你的服务器干掉线,只是看你的钱包能不能受得住。DDoS 的攻击和防御成本并不是一个量级的,一般的小站点可没有这个资金去买高防,升级服务器的性价比也很低,这是一个很糟糕的解决办法

使用 CDN

本文章的主角,能让 DNS 解析不会给出你的源站 IP,同时还能保证域名可以正常访问,它是怎么运作的呢?

CDN 翻译过来就是内容分发网络,在 DNS 层面,它可以将 DNS 解析出来的 IP 从你的源站 IP 换为来自 CDN 的 IP,避免 DNS 直接暴露源站 IP

DNS 解析出来不是源站 IP,那怎么获取我的站点内容呢?

按照流程,在 前面 所说的 DNS 解析出 IP 进行返回的过程中,使用 CDN 后,解析出来的 IP 是来自 CDN 的。接下来的请求部分,用户会从向源站 IP 发送请求变为向 CDN 的 IP 发送请求,这里 CDN 需要负责接受请求,然后再向源服务器发送请求,过程如下:

# 没有使用 CDN 的请求
用户发送请求     >   源服务器收到请求
源服务器发送响应  >      用户收到响应

# 使用了 CDN 的请求
用户发送请求    >        CDN 收到请求
CDN 转发请求    >    源服务器收到请求
源服务器发送响应  >      CDN 收到响应
CDN 转发响应    >      用户收到响应

可以看到,开启 CDN 比不开启 CDN,用户每个请求都会先到达 CDN,再由 CDN 转发到源服务器,源服务器处理完得到响应之后,再发送给 CDN,CDN 再将回应发送给用户

看上去多了两个步骤,麻烦了不少,但我们需要的隐藏源站 IP 功能不是实现了?

不过要注意,CDN 很多时候只能转发 HTTP/S 请求,例如你本来用你的域名直接解析到 IP,使用 git 克隆仓库时可以使用域名替换服务器 IP,但开启 CDN 后,就不能这样了,你可能需要找一些其他方法

还没完

我们选择了最实用的 CDN 方法来隐藏了源站 IP,但还有一些需要注意的事情

  1. 如果你在没开 CDN 之前就被打了,只开 CDN 是不行的,你还需要同步更换 IP。CDN 只是阻断了通过 DNS 获取源站 IP 的方法,但你的 IP 早就在开 CDN 前泄露了,此时攻击者不会和你走流程,直接对着 IP 继续打,服务器还是得遭殃
  2. 我没有开过 CDN,但我也并没有被打过,看了文章我现在感觉有必要开一个 CDN,还来得及吗?
    没问题,但有隐患。网上有很多专门扫域名扫 IP 的,可能已经记录了你的 IP,但情况并不坏,只要没人刻意去查,是找不到的,就算找到了,你简单换一下 IP,那些记录也就失效了

避免源服务器暴露站点特征

截止到这里,防止 DNS 泄露源站 IP 的方法其实已经解决了,不过为了安全,我们还是要配置一下源服务器上的反向代理,避免网上那些扫 IP 的服务暴露了我们在 CDN 后的源服务器 IP

排查暴露的端口

首先,我们要检查一下直接访问服务器 IP 能获得什么,请按照下面的列表排查一下自己服务器:

  • 在浏览器中通过 IP:80IP:443 能否直接访问到你的 HTTP/S 服务
  • 如果你有其他非标准端口的服务,使用 IP:端口 是否能直接访问
  • 如果你有部署 docker 或其他容器类服务,请检查容器的端口映射是否暴露在了公网
  • (可选)去 Censys 搜搜你的 IP 中有哪些被记录的端口和服务
暴露的 80/443 标准端口

如果你的 HTTP/S 服务能够通过 80/443 端口访问,那说明你的服务就直接暴露出来了,就算攻击者因为 CDN 中继保护还不知道你的 IP,但 80/443 是标准的 HTTP/S 端口,最简单的 IP 扫描也不会放过这两个端口,只要被扫到,基本就会暴露对应的数据

你可能会好奇,不是开了 CDN 吗?但 CDN 也是通过 80/443 端口才能转发服务器与客户端之间的请求,CDN 能访问到的数据,通过 IP 访问也可以,而直接通过 IP 访问则会绕过 CDN。如果一个 IP 访问后显示的网页数据和某个域名一样,那不用猜,这个 IP 就是域名后面对应的源服务器 IP,这时你的源服务器 IP 也就暴露了

还有上面说到的 Censys,有些人可能觉得我去上面搜自己的 IP 不就是主动暴露自己吗,确实有这个风险,但你可以去搜一下你的域名,有时候你的 DNS 解析或是服务器设置的证书中的域名也可能会在上面暴露 IP 与域名的关系

暴露的非标准端口

第二则是非标准端口的服务,其实也像上面一样,没什么安全性。大一点的扫描类服务为了数据库够大、相比同行更有竞争性,基本都会把每个 IP 能扫的端口都扫一遍,设定非标准端口并没有太大作用

暴露的容器端口

最后是容器中的端口映射,一般容器与主机的端口映射是可以修改的,很多容器自部署服务为了避免端口冲突,并不会将服务端口设为 80/443,而是 3000、5000 这种四位数的端口。为了让 IP 或域名可以不加端口直接访问,需要在服务器上设定反向代理,在收到请求时,根据传入的域名,决定转发请求到哪个容器中处理

设置得当时,通过域名访问站点,不管源服务器上有多少个容器,反向代理都可以帮你正确的转发请求到对应的容器,而用户与 CDN 发送的请求始终都是通过标准的 80/443 HTTP/S 端口,这可以让我们无需为每个相同的 HTTP/S 服务单独设置一个端口,避免了暴露多个端口的问题

不过,你有按照我说的步骤去排查 docker 容器端口映射了吗?如果发现你的 docker 容器也可以使用 IP:端口 直接访问,那你就要注意一下 docker 的端口映射方式了

docker 中容器的端口映射有两种方式:暴露至公网与仅在本机暴露,下面假设容器内的端口为 2340,映射到主机的端口为 1230:

// 暴露至公网
// docker run 启动
docker run -p 1230:2340 <容器镜像>
// docker compose 配置
ports:
  - 1230:2340

// 仅在本机暴露
// docker run 启动
docker run -p 127.0.0.1:1230:2340 <容器镜像>
// docker compose 配置
ports:
  - 127.0.0.1:1230:2340

相比之下就是在 1230:2340 前多加了一段 127.0.0.1:,可能有点难看懂,这时它实际也是分为两段,127.0.0.1:1230 为主机的部分,2340 依然为容器端口,这里我们主要是限定了这个端口只允许在 127.0.0.1 访问,而 127.0.0.1 同时也对应了 localhost,就仅为本机使用

修改容器配置后,重启一下容器,再运行 docker ps 看看 PORTS 是不是由 0.0.0.0:1230->2340/tcp 变成了 127.0.0.1:1230->2340/tcp。这个时候,就无法通过 IP:1230 的方式访问到 docker 中的容器了

还有什么要注意的吗?当然,现在通过 IP 的 80/443 端口依然可以访问到服务器上的服务,跟 前面 说的一样,只要通过 IP 访问依然能得到与通过域名访问一样的数据,那还是能猜到这两者的关联,所以我们要完全切断 IP 与域名的联系,不留任何可以被关联的数据

当然肯定不是让你直接关机跑路

禁止直接通过 IP 访问

看上去很玄乎?虽然前面也提到了 CDN 能访问到的数据,通过 IP 访问也可以,但也有方法,让 CDN 可以正常转发,而直接访问 IP 不返回任何数据

此段部分内容来自 NGINX 配置避免 IP 访问时证书暴露域名 - ZingLix Blog 文章,在此感谢 ZingLix

假设我们使用的反向代理软件为 Nginx,我们原本的配置如下,其中假设自己的域名为 example.com

# 此段作用为转发 HTTP 请求至 HTTPS
server {
  listen 80 default_server;
  return 301 https://$host$request_uri;
}

server {
  listen 443 http2 ssl;
  server_name example.com;
  ssl_certificate <SSL 证书公钥文件>;
  ssl_certificate_key <SSL 证书私钥文件>;
  # 忽略了一些配置

  location / {
    proxy_pass http://localhost:3000;
    # 忽略了一些配置
  }
}

一份很普通的 nginx 配置,除了转发 HTTP 请求到 HTTPS 外,还有一段配置,作用是收到包含 example.com 域名的请求时,将请求转发到本地的 3000 端口进行处理。解析好 DNS 或同时加上 CDN,你都可以通过 example.com 域名来访问到服务器上的 3000 端口对应的服务

不过,如果你通过浏览器直接访问 IP,不管使用 http:// 还是 https:// 前缀,最后都是访问到了 443 端口,因为第一端的转发请求起了作用。而下一步就是请求 443 端口了,第二段配置的 listen 443 意思就是监听 443 端口,那么这个请求自然就传到第二段配置设定的 3000 端口对应的服务里去了,这就造成了使用 IP 能直接访问到服务器上的服务,我们可不希望这样

添加配置拒绝直接通过 IP 访问时的请求

那怎么办?我们可以在第一段后面添加一段监听 443 端口的配置,返回 403 或者 404?你可以现在试一下:

# 此处作用为转发 HTTP 请求至 HTTPS
server {
  listen 80 default_server;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl default_server;
  return 403;
}

# 忽略了第三段配置

然后你会不出所料的得到类似 no "ssl_certificate" is defined for the "listen ... ssl" 的错误,简单来说就是,想要监听 443 端口,你必须要设定一个 SSL 证书,当然这个证书是否有效并不重要,只要是个证书就可以

但在添加证书前,我们先看看证书里有哪些内容(来自 什么是 SSL 证书? | Cloudflare):

针对其颁发证书的域名
证书颁发给哪一个人、组织或设备
证书由哪一证书颁发机构颁发
证书颁发机构的数字签名
关联的子域
证书的颁发日期
证书的到期日期
公钥(私钥为保密状态)

看到了吗,第一行中就有关于域名的信息,这可不行,在这个配置里加 SSL 证书也会泄露我们的域名,怎么办呢?

也有办法,只要我们给一个不包含我们域名信息的 SSL 证书就可以了,毕竟 nginx 并不会检验证书的有效性,于是我们使用 openssl 生成一个私钥和证书:

# 请在 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

运行以上命令后,目录下会多出来三个文件,分别是 private.keycert.csrcertificate.crt,将其添加到前面的配置里:

# 忽略了第一段配置

server {
  listen 443 ssl default_server;
  ssl_certificate <文件目录>/certificate.crt;
  ssl_certificate_key <文件目录>/private.key;
  return 403;
}

# 忽略了第三段配置

此时再去直接访问 IP,你就会得到证书无效的错误,当然你也可以在页面上找一找,一般会有高级选项允许继续访问,就会得到 403 或 404 的错误,看你是如何设置的

不过我们是为了防止证书暴露信息,所以这是无法访问也是在我们的预期中的。点击浏览器地址栏旁边的信息按钮,这个按钮一般会显示成一个小锁或是类似设置的图标,就能看到证书无效的提示,点击查看详细信息就在弹出的窗口里可以看到里面的证书信息,这些信息就是你在前面生成证书时填写的信息

到这里,已经很难将你的源站与域名联系起来了,只要你使用 openssl 生成的证书中没有明晃晃写上你的域名,那想通过特征找到源站对应的域名,就等于大海捞针了

如果你的 Nginx 版本高于 1.19.4,你还可以使用新功能:拒绝握手

设定直接通过 IP 访问时拒绝握手

注意:这个功能和上一个方法作用类似,你只需要选择其中的一个来设置。相对而言,拒绝握手能提供更少的信息,效果更佳

这个功能需要 Nginx 版本高于 1.19.4,你可以在终端运行一下 nginx -v 检查一下是否可以使用该新功能。当然你也可以去升级 Nginx,如果无法升级或其他原因不想升级,上面的方法也足够安全了

拒绝握手的方法配置起来很简单,与上面的方法类似,只需在正常配置前添加一个监听 443 端口的服务即可,可以不指定 SSL 证书:

# 此处作用为转发 HTTP 请求至 HTTPS
server {
  listen 80 default_server;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl default_server;
  # 当配置中开启拒绝握手时,不需要添加 SSL 证书与私钥
  ssl_reject_handshake on;
}

# 忽略了第三段配置

保存配置并重启 nginx 服务后,再去浏览器里访问 IP,就可以看到已经无法通过 IP 访问到站点了,而且页面信息里也没有证书信息,可谓非常的干净

第二段配置中的 ssl_reject_handshake on 就是关键所在,只要一段配置中启用了这个功能,那么走这个通道的请求都会被拒绝握手,不会提供任何信息,所以注意不要往正常的服务配置里启用这个功能

我们安全了?暂时的

直到这里,我们开启了 CDN、关闭了暴露的端口、禁止 IP 访问以及配置了证书避免泄露我们的域名,不过还有一个方式能访问到服务,那就是我们的域名

为什么配置了怎么多,IP 都不能访问到网站了,而通过域名还可以?下面有两行命令,你可以试一试:

curl -v -k https://<你的源站 IP>
curl -v -k https://<你的源站所绑定的域名>

第一行是直接通过 IP 请求内容,第二行相同,但走的是域名来请求内容,可能还会经过 CDN。看着我们是配置成功了,IP 会无法访问,域名访问一切正常,不过,如果你试试下面这行命令呢?

curl -v -k https://<你的源站所绑定的域名> --resolve <你的源站所绑定的域名>:443:<你的源站 IP>

看似能正常从域名获取网页内容?没错,不过我可要告诉你,这里请求的其实是 IP,而不是你的域名,是不会经过 CDN 的。但我们配置了禁止 IP 访问,为什么还是能获取到站点内容呢?

依然有办法可以通过 IP 访问

先看看这行命令有哪些参数,下面的域名将被替换为 example.com,IP 被替换为 333.333.333.333,CDN 的 IP 被替换为 444.444.444.444:

curl \
  -v \ # 输出请求时的整个过程
  -k \ # 请求时跳过 SSL 检测(不会因为证书原因导致请求失败)
  https://example.com \ # 要请求的域名
  --resolve \ # 将某个域名强制解析到某个 IP(替代 DNS 的作用)
    example.com:443:333.333.333.333 # 要强制指定的数据

这么看,这行命令的作用就是:在请求时输出全部日志并跳过 SSL 检测,跳过正常的 DNS 环节,直接将域名解析到指定的 IP 地址,再向强制解析后的域名发送请求,运行后的输出类似这样:

$ 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)
# 忽略后续信息

而正常通过域名访问的输出呢?

$ curl -v -k https://example.com
*   Trying 444.444.444.444:443...
* Connected to example.com (444.444.444.444) port 443 (#0)
# 忽略后续信息

可以看到,如果指定了 --resolve example.com:443:333.333.333.333 参数,在请求时会将 example.com 直接解析到 333.333.333.333,之后请求会直接冲着源服务器去,服务器再返回站点数据,我们的服务器可能就会再次沦陷了

怎么办呢?说实话,如果攻击者到了这种拿着域名一个个试试 IP 能不能通过请求的时候,我们就只能限制哪些 IP 可以直接访问服务器了

限制哪些 IP 可以直接访问源服务器

想要限制哪些 IP 可以访问我们的源服务器,我们总不能手动将用户的 IP 地址添加到防火墙白名单,家庭宽带很多时候并不会有固定 IP,每次用户 IP 变动时,你都需要实时将用户的 IP 添加到防火墙白名单,这相当的麻烦,而且你总不能在用户的设备上安装软件实时检测 IP 变化,这个解决方法很不现实

但如果我们使用的是 CDN,那解决方法就很方便了,由于 CDN 会负责转发用户的请求,使用 CDN 后用户与源服务器的请求始终是由 CDN 负责的,服务器日志中只会有来自 CDN IP 的请求,我们只需要使用防火墙拒绝其他 IP 的连接请求,仅允许来自 CDN IP 的请求即可

不过,你需要提前查询一下你使用的 CDN 供应商是否能提供 CDN 全部 IP 的列表,不然下方的两个方法都不适用你的情况

下面部分内容来自 屏蔽 Censys 扫描器, 及设置仅允许 Cloudflare 回源 - Zikin 的独立博客 文章,同样在此感谢 Zikin

我们有两种方式可以对设置仅允许哪些 IP 与源服务器建立连接:

方法 1: 使用 Nginx 的 allow / deny 字段 (不推荐)

由于这是一个不被推荐的方法,我并不会写出具体的操作流程,如果你还是想看看如何配置,你可以去 屏蔽 Censys 扫描器, 及设置仅允许 Cloudflare 回源 - Zikin 的独立博客 文章查看操作步骤

这个方法是可以不向被限制的 IP 发送原本的网页数据,但在搭配了上面添加的 nginx 拒绝握手特性时,会出现一个自欺欺骗人的情况。我们先保留之前的拒绝握手设置,再往普通配置里添加 deny 字段拒绝所有 IP 的连接请求:

# 此处作用为转发 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 <SSL 证书公钥文件>;
  ssl_certificate_key <SSL 证书私钥文件>;
  # 忽略了一些配置

  # 方便测试,这里我们将拒绝来自所有 IP 的连接请求,使用时,你需要改为仅允许来自你 CDN 供应商的 IP
  deny all;

  location / {
    proxy_pass http://localhost:3000;
    # 忽略了一些配置
  }
}

我们再使用 curl 发送请求,域名被替换为 example.com,源站 IP 依然为 333.333.333.333:

我们先试试直接请求 IP

$ 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

与预期相同,服务器拒绝了我们发送的握手请求,也没有提供证书,接下来我们试试强制将域名解析到源站的 IP 地址:

$ 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:
# 忽略了服务器证书内容
# 忽略了请求信息
> GET / HTTP/2
> Host: example.com
> user-agent: curl
> accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
< HTTP/2 403
< server: nginx
< content-type: text/html
< content-length: 153
<
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>
* Connection #0 to host example.com left intact

可以看到,curl 成功与源服务器握手,并输出了来自 nginx 的回应,虽然最后输出的是 403 Forbidden(无权访问) 的回应,也没有输出原本的网页信息。看上去 IP 白名单起效果了,但它并没有达到我们想要的效果,此时我们再试试将一个不存在的域名解析到源站的 IP 地址:

$ 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

从最后的输出可以看出,这个不存在的域名,触发了我们的拒绝握手配置

如果向源服务器发送请求时传递的域名正确,则可成功握手,否则将返回拒绝握手的 tlsv1 unrecognized name 错误

这代表当攻击者使用域名对我们的服务器进行扫描时,直接请求 IP 或附加的是错误的域名,那么你的服务器会正常的拒绝握手。但如果请求的是正确的域名,服务器接收了请求、完成了握手,然后返回了 403 Forbidden(无权访问)

有什么问题呢?将视角换为攻击者就好理解了:我直接请求 IP 你拒绝我的握手,用其他域名请求你也拒绝握手,但我用 example.com 你通过了握手再给我返回 403?那这个 IP 绝对跟 example.com 这个域名有关系

后面的就不用我说了吧,可以等着换下一个 IP 了

方法 2: 使用系统防火墙

在这里,我们使用基于 iptables 易用接口的 ufw 设定防火墙规则,此处部分内容同样来自 屏蔽 Censys 扫描器, 及设置仅允许 Cloudflare 回源 - Zikin 的独立博客 文章

我们照样先试试被防火墙封禁后,直接请求 IP 与携带域名请求会怎么样,先设置防火墙:

sudo ufw allow OpenSSH # 设定防火墙允许 ssh,否则可能就连不上服务器了
sudo ufw default deny # 将默认入站连接设定为拒绝

注意!如果你需要通过 ssh 才能连接到服务器,请务必按照上面的方法允许 ssh 请求,否则你可能会再也无法连接到你的服务器

此时,我们已经设定好了防火墙配置,允许 ssh 连接,并拒绝所有入站请求,接下来我们就可以通过 ufw 开启防火墙了:

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)

这时候再去试试请求服务器,你会发现不管是单独请求 IP 还是携带域名请求都无法获取到内容了,可能会提示无法连接或者迟迟没有完成请求,因为这个时候请求到达防火墙的时候直接被拒绝了,没有返回任何信息

不过这个时候再去浏览器访问域名,你会发现域名也进不去了,因为上面的规则阻止了所有 IP 的入站请求,CDN 的 IP 也同样被阻止了,所以我们还需要设定规则允许来自 CDN IP 的连接

与用户多变的 IP 不同,CDN 的 IP 一般是固定的,我们只需要将 CDN 供应商提供的 CDN IP 列表添加到防火墙白名单即可

我使用的 CDN 来自 Cloudflare,如果你使用的是其他 CDN 提供商,请手动查找对应的 IP 范围列表,按照 前面 的需求,Cloudflare 是提供全部 IP 的列表的,可见 IP 范围 | Cloudflare,我们只需要将列出的 IP 范围添加到防火墙白名单,通过域名访问的请求即可恢复正常:

sudo ufw allow from 173.245.48.0/20
sudo ufw allow from 103.21.244.0/22
# 省略后面一条条加的过程

测试的时候我觉得很麻烦,于是我写了一个 shell 脚本,发布在 GitHub Gists 上,可以一键获取 Cloudflare CDN 的所有 IP 范围列表并添加到防火墙白名单,你可以使用它来一键添加:

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 > add_cf_ips.sh && sudo bash add_cf_ips.sh --remove

对了,在添加完防火墙规则后,你可能需要运行一下 sudo ufw reload 加载修改后的规则以生效,之后再去浏览器使用域名访问,就可以正常访问了。使用 curl 请求 IP 或携带域名请求 IP,都不会返回站点信息,我们就完成了对源服务器的保护

最后的一些琐事

如果设定了防火墙白名单,那么上面 nginx 的拒绝握手配置其实也可有可无了,非 CDN 的 IP 根本无法触发到这个拒绝握手的规则,最坏的情况只能是攻击者用 CDN 的 IP 去扫服务器请求,如果真有这种情况,可以考虑换一个 CDN 提供商了

在想到防火墙添加 Cloudflare CDN 的 IP 白名单时,我还在考虑如果攻击者连上 warp 之后扫服务器怎么办,我就去查了查 warp 的 IP 段,跟 CDN 并不在一个段。这下除非去机房一台机一台机的拔网线,不然基本就不可能找得到我 IP 了

文章末尾感谢:

Cloudflare
DNS / CDN / PaaS 提供商,提供了很多好用的服务,帮我缓解了很多通过 CDN 的攻击,让我的网站不至于关站跑路

Vercel
PaaS 提供商,服务真的非常好用,尽管我的流量被攻击刷到远超 100GB 的 1.2TB 后,没有向我发送账单也没有封禁我的账号

本文提到文章的两位作者
再次感谢您的教程,让我可以学习到需要的网络安全知识

某个服务器提供商
由于安全原因我这里不能说是哪个服务器提供商,但还是在此感谢一下,不然我就没地方放自部署服务了

不知名的攻击者
从 23 年 7 月开始将我服务器打到空路由,在我建立防护后顶着 Cloudflare 一直给我刷了估计有 30m 请求。同时给我 Vercel 的免费 100 GB 套餐超额刷至 1.2TB,让我被迫学习网络安全知识

域名变动

作者 Hubert Chen
2024年2月16日 08:00

个人站点的域名将迎来一些变动,位于 trle5.dev 域名下的全部站点,其绑定的域名将被迁移到 trle5.xyz 域名下

站点迁移后使用的域名:

服务 先前域名 新域名 备注
Hubert's Gitea trle5.dev gitea.trle5.xyz
Hubert's Alist alist.trle5.dev alist.trle5.xyz
Cloudflare tunnle git.trle5.dev git.trle5.xyz

日程

从此文章发布起,当您访问任何一个使用 trle5.dev 域名或子域名的站点时,您都能在网页顶部看到一个提示,并有一个超链接将您引导到这个页面

在 4 月 1 日前,您依然可以照常通过旧域名访问 trle5.dev 下的全部站点,但我不保证功能是否依然正常

4 月 1 日后,位于 trle5.dev 域名及其子域名的站点将被 301 重定向至新域名,直到 6 月 4 日域名到期 DNS 停止解析

trle5.dev 域名过期后,可能会购入重新使用,如果域名被重新注册,您应该能在网页顶部再此看到一个链接到此页面的提示,文中应该也更新了域名的使用信息

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

作者 Hubert Chen
2024年1月9日 20:04

需要的东西:

  • 任意区 iCloud 账号
  • 能安装最新版软件的 iOS 设备

上一条需求听上去有点扯,都有另一个设备能装最新版了那还为什么装旧版软件。这另一个设备主要作只用是入库,即让软件出现在应用商店的“已购项目”中,如果实在没有也可以找朋友帮忙

  1. 在旧设备和新设备的应用商店中登录同一个 iCloud 账号
  2. 在新设备中搜索想在旧设备上安装的软件,安装对应软件,只要过了验证后,出现应用下载进度条,就可以取消了
  3. 打开旧设备的应用商店,找到“已购项目”页面,不出意料你能在里面找到刚刚的那个软件,点击安装,会提示“该软件不兼容此 iOS,是否下载兼容的最后一个版本”,选择“是”即可

注意事项

此方法只能下载软件兼容旧版 iOS 系统的最新一个版本

如果你要下载的软件从发布起就要求很高的 iOS 版本(例如 ChatGPT 最低要求 iOS 16.1),这种情况应用商店根本没有符合旧系统的旧版应用下载,那也是没办法的

如果 iOS 系统版本高于或等于目前软件适配的最高系统版本,就没法通过此方法安装旧版软件

例如 iOS 12 在今年发布,目前某个软件的系统要求如下

1.0 版本需要 iOS 9+
2.0 版本需要 iOS 10+
3.0 版本需要 iOS 11+
3.5 版本需要 iOS 11+
4.0 版本需要 iOS 12+

现在软件版本号已经是 4.0,最低要求 iOS 12

使用 iOS 9 的设备去安装该软件时,应用商店会询问你是否安装兼容该系统的的最后一个版本,选择是后,应用商店会给你安装 1.0 版本的软件。同理,使用 iOS 10 的设备会安装 2.0 版本

但使用 iOS 11 来安装时,虽然 3.0 和 3.5 都兼容 iOS 11,但应用商店只会安装 3.5 版本,因为这是兼容 iOS 11 的最新一个版本,想安装 3.0 版本就只能找其他办法了

如果使用 iOS 12+ 的设备来安装这个软件,不管你是搜索直接安装,还是在“已购项目”中安装,下载到的永远都是最新的 4.0 版本

下载后的旧版软件能正常安装不代表能正常使用,因为服务器可能会对旧版本进行禁止登录或其他限制使用的行为,这样就没有其他办法了

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

作者 Hubert Chen
2023年4月27日 17:43

有些机房的老电脑的 BIOS 电池早就没了电,还拿不到管理员权限来更改时间,通过更改策略组来给予其他用户更改时间的权限就可以调整到正确的时间了

首先按下 Win + R 键进入运行窗口,在弹出的窗口里输入下方内容来进入 本地策略组编辑器

gpedit.msc

然后根据路径找到对应的策略文件夹:

计算机配置
↳ Windows 设置
  ↳ 安全设置
    ↳ 本地策略
      ↳ 用户权限分配 > 更改系统时间

双击策略名称后,会弹出来它的属性,点击中下方的 添加用户或组(U)...,再点击下方的 高级(A)...,继续点击右侧的 立即查找(N) 按钮,就会在窗口下方显示现有的用户和组,选择要添加的用户或组点击保存即可。图标显示一个人的是用户,显示两个人的就是用户组

考虑在机房可能并不能重启,若遇到这种情况,可以考虑添加其他普通用户或来宾用户,并给予修改时间的权限,再通过开始菜单切换到对应的用户来修改时间

如果你使用的设备登录的是没有添加用户权限的用户嘛,那就给当前用户添加权限,保存后注销当前用户再登录,检查一下能不能修改时间。我自己试了试似乎不行,祝你好运吧

年间

作者 Hubert Chen
2023年2月9日 08:00

不知道应该放个什么题图,随便做一张吧,配合倒数第二三个主题看上去效果不错

换句话就是寒假期间,算年度总结的额外篇?

写文档

从去年十一月底看了 Urara 的文档后,感觉自己的水平还是能加个简体中文的翻译的,具体过程过的有些久也忘了,当时是贡献准则都没看一眼,但也看到 commit 消息如此工整,就在 PR 前去与作者交流,大致问了下,不过现在还是不怎么会写 commit 信息,还是乱一团

翻看 commit 记录,确实在假期里写了大多数的文档页面,基本是隔几天更新一次,当时的情况大致如下

  • 有文档要写:好累啊,什么时候能写完
  • 没文档写:好闲啊,今天格子要空了吗

现在大体也没什么要更新的了,要加新页面也可以套之前写过的框架,有需要更新的可能就是大部分靠 DeepL 的英文文档了,希望没有问题

继电器与 HA

这个其实从去年十一月初就在折腾,但 Home Assistant 则是在寒假开始才有折腾

通过 HA 再次体验到了 Python 的强大与…依赖地狱,不过也不是它的问题,我总是想在奇奇怪怪的设备上跑同样奇怪的东西

试过使用 LinuxDeploy,是成功跑起来了。但是 HACS 用不了,没法绑定刷了 Homekit 固件的继电器,更没法连接 HA 安卓客户端,那样就只有个服务器端在跑,控制电器的功能没用啊

后面去尝试了 Ubuntu Touch,想着这是个能在手机上跑的完整 Linux,应该没什么限制吧。结果是开头没法更新软件源,排除了一阵子后,发现是 Ubuntu Touch 16.04 更新软件源需要使用 apt-get 而不是 apt

后续我又去网上查了查,当头一棒,还有分区大小不足与镜像只读问题,就摆了

但最近又在 GitHub 上翻到了个项目,以后手痒了可能又会试试

GnuPG 与 nRF52840

最初接触 GPG 是什么时候也已经不清楚,但近期印象最深的是 SevicheCC我在看什么 · 12 月 文章,最主要的收获竟然是:

openPGP / GnuPG 的使用并不强制需要物理安全密钥

是的,我对它们也早有耳闻,但大多数文章都是先介绍 Yubikey 再介绍 GPG,让我以为拥有一个 Yubikey 是使用 GPG 的硬性要求,当时的我再去看了看价格,是买不起的玩具

得知要求后就兴致冲冲去生成密钥了,由于第一次接触,并不太了解密钥类型,就直接选了 RSA4096,虽然安全性高了,但对后面的安全密钥部分造成了影响

再接着就是看了 藍+85CD 的新文章 为 nRF52840 Dongle 刷入 CanoKey 固件,再去找找,看到亿佰特淘宝店新用户买前两个只用 18 块加运费,对比 Yubikey 或 Canokey Pigeon 就只算个零头,于是…买!

但它也不是完美的,能这么低价格买到安全密钥,也是会有缺点的

首先是安全性问题:使用 nRF52 版本绝对没有安全保证或担保,因为这是一个基于开发板的开源项目,并不是针对安全的商业产品,并没有软件与安全保证

其次是性能问题:由于我一开始创建的密钥类型选择了 RSA4096,将密钥导入到安全密钥后就无法使用,后续创建了一个基于 ED25519 的密钥并导入后 GPG 就能正常工作了,初步判定应该是性能或内闪存的原因

除夕

这次的新年是比以往热闹了很多,可能是疫情放开了的缘故吧,在除夕当天忙完了后,自己也是习惯的一个人回到了房间,倒不是说孤僻到没人理,实际上经常被亲戚与小孩来打扰,效率有些影响

再想想好像也没什么印象深刻的事情,感觉是与以前相同,但感觉又有不少改变,这样也不坏吧

人际关系

过年期间没被拉出去玩,走亲戚都很少,也就没什么新朋友,但通过博客圈和 Mastodon 认识了些网友

相比以前也是没什么大变化,与认识的网友聊还是照常,该怕生时还是怕生。年夜饭时被拉出来认了一半忘了一半的亲戚们似乎个个也是在说我要变得开朗与外向,也都只是抱着个希望,随心所欲吧

修手机

从去年年底表姐就找我诉苦,说她的手机屏幕被自己摔坏了,希望找我修修看,但现在两家住的地方离得远,也就没那么着急,当时也还在疫情当中,没有去修就一直留着

后续她买了一台新手机,又不幸在某日进了海水,起初是询问我进水了要怎么办。由于之前手机也进过水,后续晾干就没事,我就找了找拆开后壳的简单方法指示她拿个风扇对着手机吹,但她并没有告诉我进的是海水…

具体结果基本上从进海水就基本没救了,我把进水的那台手机拆开后,主板没有腐蚀到核心区,副板已经全是铜被腐蚀呈现出的铜绿色和海水晾干后的白色盐晶,得大修了吧

得知进的海水后我就转向了被摔碎屏幕的另一台手机,仔细看了看只有屏幕和后盖有问题,最低成本的维修就是换个后盖和屏幕,但我可没那个手艺,淘了带框屏幕和后盖,到货后修起来没什么难度

重装系统

拖了好久终于决定空一天出来把 Windows 盘格掉重装了,以往都是只要不炸就一直用的想法,是因为最近要升级硬件,再拖累硬件就不行了

先前使用的系统版本为 Windows 10 LTSC 2019(1809),这次经过了打算后,还是选了 LTSC,但我升级到了 LTSC 2021

同样,LTSC 2021 有个众为人知的输入法 bug,它会导致输入法缺失一个依赖库,反复唤醒 Windows Update 来尝试安装这个拓展,结果就是会导致 wsappx 进程大量占用 CPU 与内存。

之前说的方法

虽然听说这个 bug 已经被更新修复了,但去管 Windows Update 的人可是少之又少,我从 美樂地 的博客文章 中找到了解决方法,只需要去下载 VCLibs 选择对应的处理器架构进行安装即可

划掉的链接有错误,需要的并不是上面链接里的 VCLibs,需要的是下方添加商店 GitHub 仓库里的安装程序:x86 x64

但好像只安装 VCLibs 并不能解决这个占用资源的 bug,如果安装后 bug 还在,就要考虑要不要直接装商店了

如果有为 LTSC 添加商店,则可以直接双击安装,否则就需要手动在 PowerShell 管理员模式下运行安装命令:

cmd
Add-AppxPackage -Path "appx包路径"

安装后可能需要重启才能生效,过了一段时间才写的文章只能记起这么一部分了

在今年朋友找我配电脑的时候,要给他装系统时我就安装了 Windows 10 LTSC 2021,其实就是再下镜像要等。但我在折腾了大半个晚上后,发现上面这些方法都不管用,想要解决这个输入法占用问题,唯一的办法可能就只有联网获取 Windows Update 了。在等待它安装的时候可能还会资源占用过高甚至窗口无响应,请耐心等待

升级硬件

今年久违的升级了一次电脑硬件,具体如下表:

设备 型号
显卡 NVIDIA Tesla P4 8GB
内存 DDR3L 4GB
硬盘 SATA SSD 240GB
机箱风扇 后方 8CM & 侧面 12CM

除了硬盘都是网购来的,总共花了 500 左右,其中最贵的是 Tesla P4,占了 80%,至于为什么突发奇想去买 P4?首先是 19 年初买的 R9 270 在 20 年 7 月花屏退役了,之后不管是打游戏还是上网的体验都下降了一个等级,后面还有一条内存正好炸掉了,出于都这样了还不如整台换掉的想法就没去管…

后续硬件价格大涨大跌,但我也并不想当背锅的那个,期间也看到 P106 和 K20m 这种没显示输出接口的显卡靠核显输出来玩游戏的教程,那时候太火星,再去看价格时已经失去了性价比

最近看到了朋友在玩 Stable Diffusion 发嘟文时提到了 Tesla P4,习惯性的查了查,似乎性价比不错,也不怕被拐进矿坑(应该?),还有那神奇的能耗比和显存,考虑考虑还是下单了

由于服务器卡没有主动散热,还得解决散热问题,就跟着网上的教程买了个 1650 刀卡的散热,能稳当当的卡到显卡上,还需要接到主板的风扇针位,尽管卖家配了降速线,但噪音还是略大,烤了十分钟机温度在 53°C 左右,偶尔几次打游戏切出来看是 40°C 左右,满载估计破不了 70°C

现在这张显卡的价格在 400 元左右,大部分都是服务器换下来的,相比现在的矿渣游戏卡可能性价比差点,我不敢说稳定性会比矿渣好多少,但翻车时我会来更新的

性能方面我的平台很旧,甚至我现在才发现都不支持 PCIE 3.0,跑了 3DMark Demo 结果贴在这里供参考一下:

测试项目 TIME SPY NIGHT RAID FIRE STRIKE
总分 4192 20229 10434
显卡分数 4641 48861 14183
CPU 分数 2709 4682
物理分数 6331
综合得分 5192

为什么不直接贴 3DMark 自带的链接?因为当时根据 几簟生凉Tesla P4 玩游戏 文章安装的 GRID 驱动 会导致一些信息被修改,3D Mark 会识别失败,评分就会被隐藏,无法公开访问

打游戏

既然都升级硬件了,不过过游戏瘾好像说不过去了,由于之前有点受显卡花屏的 PTSD,不敢给显卡上太大负载,就算开了高画质核显也会处理不过来让帧延时变高,帧数是满的但操作很卡

首先还是玩玩 CS:GO,全局打开了垂直同步就没看出有什么区别,不过帧数就从没掉下过 60 FPS

其次去玩了玩没怎么碰的彩虹六号:围攻,好像水平已经掉到新进玩家水平了,很多地图重做后就直接迷路了,或许还是真不太会打 PVP 游戏

还有就是彩虹六号:异种(繁体译成撤离禁区),操作手感继承了围攻,以及去年年初也玩过那么几次,打一二级难度还是没问题的,就是听说好像已经停止开发了,不知道后续会开放社区内容吗,游戏重复性其实也逃不开刷刷刷,可能玩多了就会失去兴趣

重返学校

以前的时候,回到学校的感觉像是在做一场很累的梦,回家则像是醒来后的生活,现在住宿后变了,周末节假日的回家更像是梦,而在学校的时间更像是现实中的家,节假日的时间只是美好的梦…

经历了一个多月的寒假和一周的拉扯,我还是回到了学校,像是被自己嘲讽了。像是活在黑客帝国的矩阵里,整个人生已经被规划好了,却被包装成看上去有无限可能的样子,尽头却是同一个

抱歉写了那么多抱怨的话,现在我清醒了,我像是看到了我能走的路,不用过于担心,我有很多时间,很多很多!

速通闲鱼

在上方的拉扯期间,有那么点怀疑自己自学的东西有没有用,我的理想与别人安在我身上的理想一比较就显得如此无力,想起之前也有过这种事,就没多想,手机和平板直接往闲鱼上挂

虽说后面也是再买了台 Pixelbook Go,还正在用来写这篇文章,但我是真没想到这手机和平板能出那么快,以往挂手机和平板也是等了一个月往上,这次竟然不到五天就都有人拍下了,成功交易回了千来块血

Pixelbook Go 其实我之前就很喜欢,大约去年四月就在闲鱼翻到了,只是当时价格还在 1.8k 往上,现在均价 1.4k,这样就比较适合我这种垃圾佬捡了,经过挑选后,1.2k 拿下一台次顶配版,只是成色不太行,主板还有电流声,希望没事

放自己一马

如果你每天还在精神止痛的过程中挣扎,无暇顾及什么新的目标以及爱好,这个时候你更应该做的是放自己一马,而不是逼自己一把。

上面这句话来自 有时候,人生也需要一些摸鱼时刻。有时候并不是自己太颓废,只是前进的中途栽进灌木丛迷失了方向,这时要做的是停下来修整,再寻找适合的道路轻松的走出来,而不是接着猛冲,那样也许能很快脱离困境,但会变得很狼狈

接下来就修整一阵子吧,给自己找点其他兴趣试试看

2022 年度总结

作者 Hubert Chen
2022年12月28日 08:00

嗯… 又一年…

本文的内容感觉似乎很多一部分在顶栏的闲聊分类似乎都写过,这里尽量不重复写一遍

博客

既然是在博客里写的年度总结,那第一节应该先讲博客吧

文章

建站历程 也可以得知,这个博客从 2022 年的一月底就建立起来了,不过当初可以说是什么内容都没有,再到第一篇文章 搭建一个自己的博客 间隔了一个多月才写完,现在看它的文章目录,是有点不美观了,至于重写嘛,还没有打算…

仔细看,写的比较全面和文章也就两篇,都是跟博客相关的,其余的我自己也感觉有点水文章的意思,也许以后会找个方法把质量次一些的文章换个页面收集起来,甚至还有 没写完的文章 还一直摆在那

架构

博客架构从 Hexo 换到 Urara 的内容在 建站历程 也有说,现在回想,换到 Urara 本质上还是因为懒虽说配置也有些麻烦,它集成了许多功能,例如评论和当时我琢磨很久的 RSS 订阅问题,这种插件要我去自己找可能更费劲

Urara 吸引我的其实还有设计,切换页面有动画、组件颜色跟随主题颜色等这些功能我很喜欢,以及简洁的设计和有点劝退的配置环节,在下面放个项目卡片,这个是 Urara 的插件

开源社区

开源社区,即开放源代码社区,是一个可以学习与交流的地方,还可以互帮互助解决问题,给我的感觉是:好玩!

在这里放张来自 wrapped.run 的 GitHub 统计图

来自 wrapped.run 的统计图,列出了我提交量前三的仓库、语言统计、收藏与被收藏数量,关注与被关注数量,以及最近关注的三个用户和最近关注我的三个用户

菜鸟

回忆一下,最早接触到开源这个词汇,似乎是 GitHub 的 Arctic Code Vault 计划,其实是不是最早的我也忘了,应该是我印象中最早的了,看了一些关于北极代码库文章与视频,再了解了什么是开源后,兴致冲冲的去注册了个 GitHub,然后发现帐号已存在…

嗯…我也不知道我是怎么早在 18 年就注册了 GitHub 的,或许是找软件的时候找到,顺便注册了个帐号了吧

初步接触

或者说是第一次建仓库与提交吧,这个记录是在 2021 年 12 月了,当时好像是在折腾 Sailfish OS,还没购买 Sailfish X / Aliendalvik,能想到传输文件的方法只有克隆仓库

截至到本文发布,我已经帮别人代购过两次 Sailfish OS X 了

直到一月左右听说有不用服务器就可以搭建的博客,就开始试着折腾 Hexo,也是胡乱之中给自己发了个 PR,后面也只是在用来放东西的仓库上传些东西…

学习

四月多开始,我就开始常常提交代码了,大部分都是提交到私有库,其实也不是在偷偷写什么项目,就是把写的练习题答案往仓库上丢,让贡献图好看点 🥲

每个编程初学者都离不开的话题:入门学哪门语言好?

不用想,这里没答案,网上大概也不会有标准答案,还得看自己想干什么,我最后选择了 C 语言,就一直学到现在,不能说那种一天学到晚,平均一天学个三四十分钟吧,学习环境不好,我抗干扰能力不太行

C 语言有什么弊端? 目前对我来说大概就是没学完的话是写不出什么能拿上台面的软件的,可能连替换一个文本中一个数值都做不到,不像 Python 有很多库直接拿来调用,虽说效率低了点但确实挺方便的

虽然是这样说,其实回头看一下发现自己好像也有两周没碰了,最近全在折腾博客

做贡献

说实话开出这个条目我也是有点心虚,帮点小忙

编程能力可以说还几乎没有的我,能帮上的忙只有找 bug 和写文档了

今年最早一次 PR 是在七月给 FydeOS/fydeos.github.io更正了过时的链接 #16,由于这个仓库年旧失修(指被归档),似乎没有什么人注意到,直到十月底我在官方群里再次提了一句才被 Archie 注意到然后进行合并

还有给 Urara 提的 Issues 和最近在维护的 Urara-docs,今年的贡献也就只有这些了

交朋友

虽然自己是个社恐,不过能交到新朋友还是挺开心的,在请教他人和被他人帮助的时候会很感激,能帮助到他人的时候也会感觉很开心

本来想在这里介绍一下新认识的朋友,想了想没经过同意,就不放出来了

在这里感谢每位帮助过我的人,谢谢您的帮助!

游戏

桌面端

今年的游戏时间似乎是骤减,相比之前少了很多(感觉上),放一个我的 2022 年 Steam 回顾:

我的 2022 年 Steam 回顾,打开过 330 次游戏,新玩了 1 个游戏,总共玩过 10 个游戏,完成了 12 个成就

大部分时间还是在打 CS:GO,其次是我的世界,再其次…没了

在二月时算个半首发,入了 彩虹六号:异种,虽说是围攻爆发模式的延续(那时候还不知道彩六),但玩了一阵子感觉还是挺满意的,至于网上的评价嘛,我玩游戏是倾向于防守/潜入,对于喜欢对枪的玩家来说,可能就不那么满意了

其余也是在之前的文章讲过的在我的世界服务器里玩生存,不过暑假过后服务器就真的是除了人什么都有了 😇

手游端

多数还是玩桌面端游戏,手机上的消遣大多还是看博客和消息

Apex Mobile

Apex 的手游版,感觉还原的也是可以,没怎么玩过,是大逃杀类型的游戏,腾讯味有些重

美服和港服都玩过些,都是人机有玩家的话被锤的就是我了 🥲

目前已经正式版了,就是英雄数量相比桌面端少,应该会慢慢同步,感兴趣的可以去下个玩玩,国内目前还没有服务器

彩虹六号手游

育碧做了个彩虹六号:围攻的手游版本,忘记是什么时候的消息了,有在 Play 商店出测试版,很巧的被发了个测试权限,于是就下载玩了玩

建模感觉在手游里算中高级别,至于预告片里的光影看看就行,测试服里感觉实际的测试玩家很少,地图的话也就领事馆、边境、银行和俱乐部这几个,玩法还原的很好,不过目前来看,估计不久就是跟其他手游差不多,人物的装备变成锦上添花,主要看的是对枪厉不厉害…

操作效果嘛,彩六也是算有点硬核了,全部按键堆起来大概能有个五分之二屏幕(21:9),我玩的时候误触是有点严重,但大部分时候还真是会对枪就行,装备无所谓

我的世界

很少玩,虽说常玩的服务器开通了 Java 与 Bedrock 互通,不过开了正版验证,每次登录甚至掉线都需要重新登录,很麻烦

虽然也有在本地存档上玩,不过一般不是真的无聊到没事干还是不会去碰的,用手机玩游戏真的好折磨人…

这里有个开启新版操控的资源包,有类似手柄的摇杆,下载文件然后使用我的世界打开并在资源包中启用,然后到触控设置选项里应该就能看见相应的设置:new touch control_lite.mcpack

分形空间

最近 Play Store 限免的游戏,通过 Google Play 限免信息 频道得知,截至文章发送前仍可领取,Telegram 真是个获取信息的好地方

游戏玩法类似传送门那种解密,喷气背包和能远程操控开关的点击枪很有意思,还有一个优点是画质和分辨率能开的很高,第一次拿 Xperia 的 4K 屏幕玩游戏,体验很不错,不过小工作室,关卡只有两关

成就给的经验很多,可以提升 Play 游戏等级

观影

想起来是得记录一下看过哪些电影和番剧,平时很少看,但一年积累下来也不算少了

已看完 电影居多

The Matrix 黑客帝国 1999~2021
老牌科幻电影

Hardcore Henry 硬核亨利 2015
第一人称电影,含有血腥暴力裸露元素

7 Days 七日复仇 2010
不推荐看,理由是过于暴力,剧情还行 真的不要好奇去看!

Wrath of Man 人之怒 2021
犯罪惊悚片,未删减版可能略为血腥

NO GAME NO LIFE ZERO 游戏人生零 2017
从时间线来看是 游戏人生 的前传,剧情忘的差不多了…

未看完 很多很多

看一半没有接着看完已经快习惯了…

ぼっち・ざ・ろっく! 孤独摇滚! 已看 5 集
女高中生组团玩乐队,大概印象最深的就是小波奇了,但我社恐还没那么严重

SPY×FAMILY 间谍过家家 已看 10 集
侦探家庭喜剧,间谍读心者与杀手,剧情很有趣

地縛少年花子くん 地缚少年花子君 已看 6 集
奇幻类番剧,讲了很多怪异的故事,画风和剧情都很喜欢

NO GAME NO LIFE 游戏人生 已看 4 集
算个穿越番,但这个世界有些不同

パリピ孔明 派对咖孔明 已看 1 集
也是玩音乐的番,不过诸葛亮穿越过去了

先輩がうざい後輩の話 关于前辈很烦人的事 已看 1 集
恋爱喜剧,不过更像是社畜番

也许还有,但印象少到可能都想不起来了…

看书

书没读多少,都是零零散散的看

C Primer Plus(第6版)中文版 已看 40 %
四月的时候找来入门 C 语言,讲的很通俗易懂,就是有时候翻译的编程题看不懂是什么要求…

Unix & Linux 大学教程 已看 41 %
这本书要比 C Primer Plus 看的早一些,但不常看,所以现在 Linux 的技术水平只能说还行

学校让人沾染上的 100+ 恶习 已看完
或许不属于出版物的书籍,内容也仅仅只有五十多页,不过我认为很值得一看,你在学校教育中是否丧失了什么?

音乐

主要在 Spotify 听音乐,虽说网易云也在用,不过我用国内软件都不会去更新,算了

Hubert Chen 的 2022 年听歌排行
Spotify 的听歌排行,有 101 首,最近发现好像有些歌莫名奇妙被移出收藏歌曲了,奇怪

不知道我喜欢的音乐人 Daxten 怎么不出新歌了,最后的音乐更新停在 2021 年…

Spotify 竟然也可以像网易云一样在网页嵌入专辑了,试一试,现在换成根据组件改的 Urara 插件了,体验应该没差别

很喜欢的一部纯音乐专辑,很轻快的感觉

Daxten 的第二部专辑,目前也就只有两部专辑,这个专辑是有 Epidemic Sound 唱片公司的其他歌手合作制作的

Epidemic Sound 唱片公司有很多其他歌手我也很喜欢,例如 Tape MachinesCraig Reever,不知道是音乐风格相同还是 Spotify 推荐算法里有这样一套,我听的推荐音乐内同一个唱片公司的艺人是真不少

外出游玩

虽然是宅,其实有时也会出去的,但也只有逛逛公园了

图片经过我认为不怎么会损害画质的压缩,已经尽力了,本来想试试 .avif 格式来看看压缩效果会不会好些,但软编码 avif 性能非常差劲,往后推了吧

逛过几次公园,虽然拍了很多,但放几张拍的好看的就行了

在步行梯扶手处对着天空斜上方拍了一张图片,可以看到步行梯的扶手和台阶地板是木头做的,栏杆则是刷了白漆的铁栏杆

在一个很多步行梯的地方往上拍,不太懂这个建筑应该叫什么

一个很有意思的屋子,在一家商铺上方凸出了一个被像是绿色藤蔓覆盖了的阳台?似乎不那么透光,顶上也有封起来,还有一张深绿色的厚塑胶片盖住了顶部防水

去往另一个公园的路上遇到的有趣建筑,表面被像是藤蔓的植物还是人造的东西覆盖了,不知道支撑材质是什么,感觉是钢架子做的

一辆作为公园景点的坦克,不知道已经在这里放了多久,早已锈迹斑斑

作为公园内景点的坦克,记得某位朋友喜欢坦克,就拍了下来,估计在这里放了很长时间

疫情

本来是不怎么关心的,但今年 12 月底那个操作,是突然让我有点反应不过来

疫情开始

翻翻 Google 相册回想一下 2019 年底吧,我的记性都快分不清哪些事是在什么时候发生的了,19 年底的时候武汉刚说检测到病毒,当时网上说是类似 SARS 的冠状病毒,我还并没感到什么,可能是从未经历过这种大感染

疫情对我的影响,最早是进出学校佩戴口罩,但当时还没有组织核酸检测,有症状的自己看着办就行

不过上面说的是 2019 年寒假后,这段寒假期间挺折磨我的,寒假加长到四月,春季学期的课程被压到要在两个月内上完,寒假被管控在家时,还有各种压力,感到交流无效的可悲,就一年没怎么说话,头发也留到了能盖过侧脸

它改变了我?

不如说是家里的管教方式改变了我,我在那段开始考虑人生的意义,有时同学们去上体育课,我一人留在教室,思考为什么如此堕落?

是有点不良少年的意思,但现在来看,当时做的还行,在那段一年的沉默期后,父母不会想着如何让我少碰电子设备、只一心栽进枯乏的学校教育里了

同样改变的还有我们家族,从我记事起,每到新年,我们家族的人都会聚在一起,进行走访祭祖后,晚上聚在一起吃年夜饭,小孩子们会聚到一起玩烟花,我也能混进去

但都是过往了,2020 年后,家族的每户人都只会在自家吃年夜饭了,就算在年底放开了管控,还能把家族的人聚到一起吗…

今年 今年如何?一如既往或更烂

今年具体的活动我都有在之前文章写过,或许没有说到疫情

以往我家居住区附近是没有什么核酸集体检测点的,想测核酸只能驱车前往六七公里外的医院,但后面把原有的篮球场当成了检测点,居住在附近的居民都要去做

暑假两个月,我一直在家,不说从未,但也是极少出门了,父母硬要拉我去做核酸,似乎病毒会渗透进我房间,有点像网上说的瘫痪老爷爷被好心人抬去做核酸,经过好心人的不懈努力,终于检测出阳性了

结果嘛,暑假我这边被封了一个月,后半个暑假都在玩游戏,具体请看 2022 下半年的总结

放开管理后

从感冒开始请假回家一周,回到学校没几天突然就说放开了,有种出来早了的感觉,但也不是很难受,因为没过多久,就给我们放回去了

从网络上也能知道个一二,疫情在国内基本也炸开了,截至本文发布,我家里已经有两例阳性了,可能我在不久也会成为其中一个吧,也可能我才是那个无症状感染源 😰

对于突然放开的看法吗,感觉是挺正常的,也是迟早的事。过去三年了,差不多是能被影响的东西都被影响了,突然放开也不说得上是紧急避险,经济倒退各种问题是在所难免了。好处我是想不到什么好处,让统计数据数据好看些了?

放开管理后我应该也并不会有什么太大的变化,可能会少些出门在外的限制吧,别说口罩,我清楚记得之前出门可是连手机都可以不用带上

新的一年

希望生活不会越来越烂吧,同时也应该给自己立些目标

计划在大多数时候是赶不上变化的,我说话时很多时候会带上 大概 好像 可能 类似的不确定词,凡事没绝对嘛,所以我立目标也不敢立的太多…

  1. 学完 C 语言 (应该能学完吧)
  2. 保持对画画的兴趣
  3. 考虑下一门语言?目前想法是 GoSwift
  4. 可能会考虑试试前端,从最近折腾博客来看是挺好玩的
  5. 试着写点什么东西?学完 C 之前大致写不出来
  6. 多参与开源社区的贡献
新年许个愿?

新年是应该有个愿望,但想了很久,发现最后心中的声音还是那句听过了很多次的话:

希望每个人都能平平安安的


那么,这就是年度总结的全部了,希望我今年会比以往努力一些吧,一点点也好 🫠

Urara 拓展插件

作者 Hubert Chen
2022年12月21日 08:00

近期加了 Giscus 后发现还有很多拓展可以加,下面也附上部分配置的教程

本文包含的大多数拓展已包含在官方文档内

查看官方拓展文档:拓展 | Urara Docs

文章组件

在文章里用的组件,可以像 HTML 代码一样直接插入到文章内

YouTube 视频

此教程在官方文档里也有:YouTube | Urara Docs

Urara 默认包含这个拓展,使用方法只需要在文档内导入一下就可以用了

ts
<script>
import YouTube from '$lib/components/extra/youtube.svelte'
// 若有其他拓展组件就往这里加
</script>

使用的时候点进一个 YouTube 视频,看到地址栏有类似 watch?v=WysuxO4yR04 的部分时,复制其中的 WysuxO4yR04 视频 ID 既可

然后在文章内添加一行

ts
<YouTube id="WysuxO4yR04" />

再把 WysuxO4yR04 替换成你想要展示的 YouTube 视频既可

资料卡片

此教程在官方文档里也有:资料卡片 | Urara Docs

首先要去下载 profile.svelte,下载完成之后放进 src/lib/components/extra/ 目录里

像上面一样,使用前也得要在文档内导入

ts
<script lang="ts">
import Profile from '$lib/components/extra/profile.svelte'
import YouTube from '$lib/components/extra/youtube.svelte'
// 上面这行是模拟了你同时导入了两个组件的情况,实际使用时请删掉
</script>

使用方法也是同上

ts
<Profile subname="这里是姓氏" bio={`这里是简介。<br>这是第二行简介。`}/>
// 更高级一点的?你也可以在里面手动指定全部信息:
<Profile name="姓名" avatar="/assets/maskable@512.png" subname="这里是姓氏" bio={`这里是简介。<br>这是第二行简介。`} />

还可以在里面放 HTML 代码,甚至也可以套组件本身:

ts
<Profile name="姓名" avatar="/assets/maskable@512.png" subname="这里是姓氏" bio={`这里是简介。<br>这是第二行简介。`} >
<YouTube id="WysuxO4yR04" />
<iframe style="border-radius:12px" src="https://open.spotify.com/embed/album/5THlVUJAn3kq087DxcWTTa?utm_source=generator" width="100%" height="352" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>
<Profile name="姓名" avatar="/assets/maskable@512.png" subname="这里是姓氏" bio={`这里是简介。<br>这是第二行简介。`} />
</Profile>

状态提示

此教程在官方文档里也有:状态提示 | Urara Docs

就是本文章开头那个提示栏,它包含在 Urara 内可直接使用,导入:

ts
<script>
import Alert from '$lib/components/extra/alert.svelte'
</script>

使用方法:

ts
<Alert status="warning" description="警告信息" title="警告标题"/>

statusinfosuccesswarningerror 四个选项,可以根据需要自己选择,不填默认就是 info 图标,而且会没有强调色

至于为什么我的使用例不填 description 不会出现一行 undefined?因为我看好像有时候并不需要标题,就改掉了

方法是打开 src/components/extra/alert.svelte 文件,把第二行和第三行的 undefined = 'undefined' 改成 undefined = ''

Spotify 音乐

此教程在官方文档里也有:Spotify | Urara Docs

昨天自己照着 YouTube 视频 改出来的,应该没什么 Bug 了,后面会考虑要不要加到官方文档里去 已添加

已经包含在 Urara 里了还混了个贡献者,要使用直接导入既可

导入:

ts
<script lang="ts">
import Spotify from '$lib/components/extra/spotify.svelte'
</script>

使用方法:

ts
<Spotify type="album" id="0vXB2JFdOphGK7ybYLXSRI" compact={true} theme={true} width="100%" />
  • id 为播放清单的 ID,在 Spotify 分享链接时可以看到

  • type 是 ID 的类型,有 artist album track 三个选项,默认定义为 "track"

  • compact 是卡片布局,默认定义为 {false} 常规布局,改为 {true} 为紧凑布局

  • theme 为组件背景,默认为 {true},若改为 {false} 就会让背景变为默认的灰色

  • width 为卡片宽度,默认定义为 100%,不加 % 时就是像素宽度

嗯,从这混乱的组件就可以看出,质量不咋样,我的想法是 compacttheme 能通过 truefalse 来控制,但搞了好久不知道怎么声明布尔变量,后面再修吧…

又麻烦大佬帮我修了,这下应该不会出现问题了

SoundCloud 音乐

此教程在官方文档里也有:SoundCloud | Urara Docs

Urara 最近新加的拓展,测试的时候去 SoundCloud 复制了一下原本的嵌入码,那是真的长,格式化代码后都要看一阵子

导入拓展:

ts
<script lang="ts">
import SoundCloud from '$lib/components/extra/soundcloud.svelte'
</script>

使用方法:

ts
<SoundCloud type="playlist" id="1259265289" />

上面这个样子也是最简单的样式,下面是一些自定义项

  • type ID 类型,默认是 track,若分享的是播放列表则需要修改为 playlist
  • visual 默认为 {true},禁用后若是单曲那么组件宽度会变矮,播放列表的话封面就会变小
  • color 颜色,默认是 #ff5500 (16进制值),其实改的是播放按钮的颜色,使用时忽略 # 号
  • autoplay 自动播放,加载组件后会自动开始播放音乐,默认为 {false}
  • width 宽度,同样支持百分比和像素宽度

这个最难的在于抓取歌曲 ID,它不显示在地址栏,需要手动点击分享,再选择嵌入,复制代码后,里面会有一串数字,那就是需要的 ID 了:

SoundCloud 嵌入代码长得很,这里只截取了 iframe 的部分
html
<iframe
width="100%"
height="450"
scrolling="no"
frameborder="no"
allow="autoplay"
src="
https://w.soundcloud.com/player/?url=
https%3A//api.soundcloud.com/playlists/
1259265289
&color=%2322ecf1&auto_play=false&hide_related=false
&show_comments=true&show_user=true&show_reposts=false&show_teaser=true">
</iframe>

获取到 ID 后替换掉上方示例里的 ID 既可,同时需要注意分享的类型,不然会指向错误的页面

GitHub 仓库

此教程在官方文档里也有:GitHub 仓库 | Urara Docs

一个用来展示 GitHub 仓库的组件,组织和个人的仓库都可以,只能展示公开仓库,没授权访问私有仓库的功能

还未包含在 Urara 里,就要麻烦手动下载组件了:github.svelte,再放到 src/lib/components/extra/ 目录里。

在使用前导入:

ts
<script>
import GitHub from '$lib/components/extra/github.svelte'
</script>

这个组件使用起来很简单,填入用户或组织名,再填写名下的仓库名就可以了:

ts
<GitHub user="importantimport" repo="urara"/>

网页拓展

此分类可以拓充博客页面

Friends 页面

此教程在官方文档里也有:友链 | Urara Docs

这个来说相对简单,复制几个文件再照着改就行

首先下载 friend.svelte 文件,放进 src/lib/components/extra/ 文件夹内

再下载 +page.svelte 文件,放进 src/routes/friends/ 文件夹内,src/routes/ 里默认是没有 friends 文件夹的,请手动创建并将文件放入其中

对了,还需要安装 svelte-bricks 依赖

bash
pnpm add -D svelte-bricks

添加后记得运行一下 pnpm i 再进行开发服务器测试

接下来是最重要的一步,在 src/lib/config/ 文件夹中,创建一个名为 friends.ts 的文件,再复制以下内容粘贴保存,样式来自 ./kwaa.dev 博客的 GitHub 仓库 (太长了,删掉了一些)

src/lib/config/friends.ts
ts
export interface FriendOld {
// hCard+XFN
id: string // HTML id
rel?: string // XFN, contact / acquaintance / friend
link?: string // URL
html?: string // HTML
title?: string // 标题
descr?: string // 描述
avatar?: string // 头像
name?: string // backwards compatibility
}
export type Friend = {
id: string // HTML id
rel?: string // XHTML Friends Network
link?: string // URL
html?: string // Custom HTML
title?: string // 标题
name?: string // 人名
avatar?: string // 头像
descr?: string // 描述
class?: {
avatar?: string // 头像类名
img?: string // 图片类名
}
}
export const friends: Friend[] = [
{
id: 'kwaa',
rel: 'friend',
link: 'https://kwaa.dev',
html: `<div class="card w-screen max-w-[24rem] bg-base-100 bg-gradient-to-tr from-primary to-accent text-primary-content shadow-lg transition-shadow duration-500 hover:shadow-2xl">
<div class="absolute top-4 rotate-6 text-4xl font-bold leading-tight opacity-10">藍+85CD<br />./kwaa.dev</div>
<div class="card-body p-4">
<div class="flex items-center gap-4">
<div class="avatar mb-auto w-20 shrink-0">
<img class="rounded-xl" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAVFBMVEUAAADW29T///+5wcc4ODjz8OZVVVX/7MkVFRXtrpqTJiVHKxPZOjr/+/QICz3/07d1TCNErbkaSXtgERn59vb//OPSzMzuuwKZne20srL0mIyWGwWygNgKAAAAAXRSTlMAQObYZgAAAOtJREFUKM9djwuSwyAMQ2vsBDBJyK/tfu5/z5W6aYZUMB7QQwZuhwIkwnq7KIgqTFQJV3/USQX+OoE0fqfLIh0kCtKC31VGEEGqBezdydixfAARnNeRKZDmClE4mFE/gAKpTjFOcgEde01fa4wrDjRAlGNf7vdF5QKEzSJUCRpftmFwrbW6vEGYXwEzHzi2BvCF1ve9/7hvDSgqBJYQeHxrA4oqAskA9ocpwUkIkrvbnppvlNnMUkn+9B4r+CdAAACVM5yJnBnA9U+8wCzncPgRynP6V+WGIQZyzMWspwzrTEASSi4F/kHCy/4DaDYJuEU/v5oAAAAASUVORK5CYII=" alt="藍#+85CD" />
</div>
<div class="card-title flex-1 flex-col items-end gap-0">
<span class="p-name text-right">藍+85CD</span>
<span class="text-right opacity-50">./kwaa.dev</span>
</div>
</div>
<div class="p-note prose opacity-70">ゴミ溜めで埋もれたまま、星空を眺めてるよ</div>
</div>
</div>`
},
{
id: 'test4',
name: ':hatsunemiku: 藍 :hatsunemiku:',
title: '~/kwaa.moe',
link: 'https://kwaa.moe/@kwa',
descr: 'ゴミ溜めで埋もれたまま、星空を眺めてるよ',
avatar: 'https://kwaa.moe/media/975fc04911e242147be77b60b93839b6dd1a317112717562944e3c7aef1f0203.png'
},
{
id: 'test5',
name: '藍',
title: '藍藍藍藍藍',
link: 'https://kwaa.dev',
descr: 'without avatar'
}
]

演示图

三个朋友卡片样式,其中有一个的背景使用了 Daisy UI 主题的第一和第三色做了一个左下角到右上角的渐变

可以看到,这个好友卡片有两种样式(页脚不算),看源文件也能看出来,有渐变底的那个卡片里有 HTML 格式的代码,我不会改,如果你有能力可以试着自己改, kwaa 大佬提供了一个 Tailwind Play 用于参考与修改

这里我们主要讲默认样式如何修改

依然是放一个卡片样式模板用于修改,这里是我的个人卡片 😝

ts
export const friends: Friend[] = [
{
id: 'trle5', // HTML ID,不会显示在卡片上
rel: 'friend', // 联系人类型,可选 contact / acquaintance / friend,目前不太清楚有什么用
title: 'Hubert's Blog', // 标题,显示在昵称下方
name: 'Hubert Chen', // 昵称
link: 'https://trle5.xyz/', // 点击卡片后访问的页面
descr: '你好呀 👋', // 头像下方的网站描述
avatar: 'https://trle5.xyz/assets/avatar/70455873_p3.webp' // 头像,也可调用其他网页的图片
}
]

高亮行使用了 Unicode 码,请将 Hubert's Blog 中的 '手动替换为 \u0027

效果图

我自己的朋友卡片,描述是 你好呀👋

也可以随时找我申请友链,在 关于我 页面使用任意方式联系我即可

朋友卡片的社交平台图标

应该有些人发现在我的 朋友页面,点进卡片时,有些并不是博客链接,而是社交平台的链接,于是我就在朋友卡片右下角加了个社交软件的图标

嗯,这个功能是我魔改出来的,其实本来还打算再加个社交平台的名称,最后只做成这个样子,没有前端知识,只会照着模板改

如果你有加这个小图标的想法,可以看看我博客仓库的 BE5D947 这个提交中对 friend.sveltefriends.ts 的修改,至于图标我是从 ICONS8 上下载的

上面这个图标在近期可能会改一下,看看能不能换个其他方便一点的办法

这个图标与朋友描述占同一行,它们可以同时存在,也可以单独出现一个,社交平台图标路径的使用方法类似头像,贴上图标路径加上就行

src/lib/config/friends.ts
ts
export const friends: Friend[] = [
{
id: 'HTML ID',
rel: 'firend',
title: '标题',
name: '昵称',
link: '链接',
descr: '描述',
social: '<社交平台图标路径>',
avatar: '<头像路径>'
}
]

项目展示

此教程在官方文档里也有:项目展示 | Urara Docs

SevcheCC 制作,可以去看看 为博客写一个Project showcase 页面Projects | Seviche.cc

配置过程与友链页面差不多,也是要下两个文件和自己配置一个

首先下载 projects.svelte 文件,放进 src/lib/components/extra/ 文件夹内

再下载 +page.svelte 文件,放进 src/routes/friends/ 文件夹内,src/routes/ 里默认是没有 projects 文件夹的,请手动创建并将文件放入其中

再到 src/lib/config/ 目录新建一个 projects.ts 文件,复制以下内容粘贴到文件内:

ts
export type Project = {
id: string
name: string
tags?: string[]
feature?: string
description?: string
img: string
link?: string
}
export const projects: Project[] = [
{
id: 'urara', // HTML ID
name: 'Urara', // 项目名
tags: ['Svelte', 'TypeScript'], // 标签
description: // 描述
"🌸 Sweet, Powerful, IndieWeb-Compatible SvelteKit Blog Starter. [δ](Delta)",
feature: 'Svelte', // 特点
img: 'https://github.com/importantimport/urara/raw/main/urara/hello-world/urara.webp', // 项目图片
link: 'https://github.com/importantimport/urara' // 链接
}
]

一个与页脚等宽的项目卡片,左侧是展示图片,右侧是介绍信息

目前这个拓展不能不放图片,图片框架有最小限制,图片尺寸不够的话会顶部居中对齐,图片过高则会往下纵向拓展,宽度固定

评论功能

Webmention

此教程在官方文档里也有:Webmention | Urara Docs

一种让一个网页可以与另一个网页进行交互的协议?其实原理我也不太懂,也可以跟 Mastodon 这种 Fediverse(联邦宇宙) 平台进行互动,但本站目前并没有这个功能…

还有一个发送 Webmention 链接的功能,我不太会用,就没什么说明。同时还支持基于 commentpara.de 的匿名评论功能,但似乎与目前的 Urara 依赖有兼容性问题,我降了 sveltekit 与 vite 版本后才能正常工作

这个组件默认包含在 Urara 里,所以就只用改配置,首先看 src/lib/config/general.ts 文件,大概在百来行的位置:

src/lib/config/general.ts
ts
export const head: HeadConfig = {
custom: ({ dev, post, page }) =>
dev
? []
: [
// IndieAuth
'<link rel="authorization_endpoint" href="https://indieauth.com/auth">',
'<link rel="token_endpoint" href="https://tokens.indieauth.com/token">',
],
me: ['https://github.com/<用户名>']
}

要改的也就是高亮行,把 <用户名> 改成你的 GitHub 用户名,再到 GitHub 的个人页面里修改个人信息,在 Website 框内填入你的网站域名,后面就可以使用 IndieAuth 登录了

接下来是添加 Webmention 评论组件,编辑 src/lib/config/post.ts 文件:

src/lib/config/post.ts
ts
import type { PostConfig } from '$lib/types/post'
export const post: PostConfig = {
comment: {
use: ['Webmention', '其他评论系统'],
style: 'boxed', // 评论系统栏样式: none / bordered / lifted / boxed
webmention: {
username: '[在此输入域名]',
sortBy: 'created', // 排序方式: created / updated
sortDir: 'down', // 排序顺序: up / down
form: true, // 启用评论: true / false
commentParade: true // 启用匿名评论: true / false
}
}
}

填写域名和调整设置后对博客进行部署就可以去测试了,匿名评论不可用那就用 Webmention Rocks! 来进行测试吧,要看评论的话要登录 Webmention.io,这就是为什么前面要设置 IndieAuth 的原因

Webmention.ioSettings 页面提供了评论订阅链接,它类似下面这样:

text
https://webmention.io/api/mentions.atom?token=0123456789ABCDEF_ghIJK

其中的 0123456789ABCDEF_ghIJK 是 API Key,用处就是查看所有 Webmention,保密与否看你自己,上面的链接可以使用 RSS 阅读器来订阅

还可以设定屏蔽来自某个域名的 Webmention 或删除评论

Giscus

此教程在官方文档里也有:Giscus | Urara Docs

本站就在用,依赖于 GitHub 项目仓库的 Discussions 功能,注定了对于国内的网络有点难访问,有时候可能会串评论,需要手动刷新

好消息是,Urara 自带 Giscus 拓展,这里我们就不需要像其他拓展一样手动去找文件了,只需要修改已有的文件既可

首先需要去 Giscus.app 配置一下,跟着提示的步骤走就行

至于 页面 ↔️ discussion 映射关系 这个怎么选都行,它目前并没有被分离出来,需要修改的话我后面会说

配置完成后,大概会给你一个类似下面的代码块,我们只需要其中的一些数据

html
<script src="https://giscus.app/client.js"
data-repo="interstellar750/hexo_s"
data-repo-id="R_kgDOHTJG_w"
data-category="General"
data-category-id="DIC_kwDOHTJG_84CS2Mz"
data-mapping="pathname"
data-strict="0"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="top"
data-theme="preferred_color_scheme"
data-lang="zh-CN"
data-loading="lazy"
crossorigin="anonymous"
async>
</script>

接下来的就是对应的上面选项的样式,根据是否相同,填入 src/lib/config/post.ts 文件既可

测试前请注意把仓库和分类的 ID 改掉,不然你的评论会发到我这里来 😱

src/lib/config/post.ts
ts
import type { PostConfig } from '$lib/types/post'
export const post: PostConfig = {
comment: {
use: ['Giscus'],
giscus: {
repo: 'interstellar750/hexo_s',
repoID: 'R_kgDOHTJG_w',
category: 'General',
categoryID: 'DIC_kwDOHTJG_84CS2Mz',
reactionsEnabled: true,
lang: 'zh-CN',
inputPosition: 'top',
theme: 'preferred_color_scheme'
}
}
}

关于 data-loading="lazy" 这个选项嘛,它其实已经默认启用了,如果您想要修改的话,可以查看 src/lib/types/post.ts 文件,将其的值修改为 eager 或直接删除它来禁用懒加载(不过我没试过有没有效果 😝)

src/lib/types/post.ts
ts
⬆57
/** choose the language giscus will be displayed in. */
lang?: string
/** loading of the comments will be deferred until the user scrolls near the comments container. */
loading?: 'lazy'
}
export type UtterancesConfig = {
⬇65

如果您需要修改 页面 ↔️ discussion 映射关系 的话,您需要修改 src/lib/components/comments/giscus.svelte 文件里的内容,可能以后也会移至 post.ts 里吧 大概不会了

src/lib/components/comments/giscus.svelte
ts
⬆6
onMount(() => {
const giscus = document.createElement('script')
Object.entries({
src: config.src ?? 'https://giscus.app/client.js',
'data-repo': config.repo,
'data-repo-id': config.repoID,
'data-category': config.category ?? '',
'data-category-id': config.categoryID,
'data-mapping': 'pathname',
'data-reactions-enabled': config.reactionsEnabled === false ? '0' : '1',
'data-input-position': config.inputPosition ?? 'bottom',
⬇18

根据前面说到的有时候会串评论的问题,我这里就把 data-mapping 改成了 og:title,其实有没有效果我自己都有点不清楚

现在还有串评论的问题就改回来了,留着 pathname 似乎是比较好的选项,不过依然有个小问题,例如我的 关于我 目录下还有三个文章,当 about/ 这个页面没有单独开一个讨论时,子目录里有其他页面已经开了讨论页面,那么里面的评论就会串到父文章来,不过也有解决方法,进入对应仓库的 Discussions 按照 giscus app 的格式开一个新讨论就行

Utterances

此教程在官方文档里也有:Utterances | Urara Docs

同样是基于 GitHub 服务的评论系统,但 Utterances 用的是 Issues 功能,如果有 Issues 功能需求的就不要用了,也可以把评论仓库换到其他仓库,本质上更推荐用 Giscus

首先是访问 utteranc.es 进行配置,跟着说明走就行,只是没有多语言,后面就会得到一个 HTML 格式的代码:

html
<script src="https://utteranc.es/client.js"
repo="[在此输入仓库]"
issue-term="pathname"
theme="preferred-color-scheme"
crossorigin="anonymous"
async>
</script>

再接着编辑 src/lib/config/post.ts 文件,加入 Utterances 评论系统:

src/lib/config/post.ts
ts
import type { PostConfig } from '$lib/types/post'
export const post: PostConfig = {
comment: {
use: ['Utterances', '其他评论系统'],
style: 'boxed', // 评论系统栏样式: none / bordered / lifted / boxed
utterances: {
repo: '[在此输入仓库]',
lable: '', // 标签
theme: 'preferred-color-scheme', // 主题
}
}
}

之后部署博客,在网页上登录 GitHub 并授权,试一下能否正常工作,不要忘记在设定的评论仓库安装 utterances app

界面组件

目前就功能按钮这一类

功能按钮

此教程在官方文档里也有:功能按钮 | Urara Docs

配置起来很简单,只需要放文件就行,组件放在 importantimport/urara-docs,下载想要添加的组件

然后到 src/lib/components/ 目录里新建一个名为 actions 的文件夹,再把下载好的按钮组件丢进去就行

重启开发服务器或构建后,点进一篇文章就可以看到左侧的按钮了。使用手机或窗口宽度不足的话,按钮就会隐藏起来

❌
❌