<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Posts on 蛙声一片</title><link>https://rwong.tech/post/</link><description>Recent content in Posts on 蛙声一片</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Tue, 21 Nov 2023 00:00:00 +0800</lastBuildDate><atom:link href="https://rwong.tech/post/index.xml" rel="self" type="application/rss+xml"/><item><title>在 2023 年，如何防止 CSRF 攻击</title><link>https://rwong.tech/post/mitigate-csrf-attack-in-2023/</link><pubDate>Tue, 21 Nov 2023 00:00:00 +0800</pubDate><guid>https://rwong.tech/post/mitigate-csrf-attack-in-2023/</guid><description>&lt;h2 id="tldr">TL;DR
&lt;/h2>&lt;p>如果：&lt;/p>
&lt;ul>
&lt;li>你的前端应用只使用 XHR / Fetch API 请求后端接口&lt;/li>
&lt;li>完全不使用 &lt;code>&amp;lt;form action=&amp;quot;&amp;quot;&amp;gt;&lt;/code> 这种浏览器内置的提交表单的方式&lt;/li>
&lt;li>完全不需要跨域&lt;/li>
&lt;/ul>
&lt;p>那么：&lt;/p>
&lt;ol>
&lt;li>前端请求时带上一个&lt;strong>自定义的请求头&lt;/strong>，例如 &lt;code>X-CSRF-PROTECTION: 1&lt;/code>。&lt;em>头的值不重要。&lt;/em>&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">fetch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/your/sweet/api&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">method&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;POST&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">body&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;{&amp;#34;genshin&amp;#34;:&amp;#34;impact&amp;#34;}&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">headers&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;X-CSRF-PROTECTION&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;1&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="2">
&lt;li>服务端收到请求时，检查是否存在这个&lt;strong>自定义的请求头&lt;/strong>。如果存在则放行，否则不放行。&lt;em>头的值不重要。&lt;/em>&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 比如说你用 Koa 的话
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">use&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kr">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kr">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;x-csrf-protection&amp;#39;&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">ctx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">status&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">400&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">ctx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">message&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;You may be a victim of CSRF.&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">next&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>完事！&lt;/p>
&lt;h2 id="如果我需要跨域怎么办">如果我需要跨域怎么办
&lt;/h2>&lt;p>你需要这样配置 CORS 头：&lt;/p>
&lt;ul>
&lt;li>&lt;code>Access-Control-Allow-Origin&lt;/code>: 你的前端页面的 origin，例如 &lt;code>https://homesweethome.com&lt;/code>。&lt;strong>不可以设置为 &lt;code>*&lt;/code>。&lt;/strong>&lt;/li>
&lt;li>&lt;code>Access-Control-Allow-Headers&lt;/code>: 你的自定义请求头 key，例如 &lt;code>X-CSRF-PROTECTION&lt;/code>。&lt;strong>不可以设置为 &lt;code>*&lt;/code>。&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>目的在于，不可以让来自其他网站的请求可以带上你的自定义请求头。所以必须限制只有来自你的前端页面的请求可以带上自定义请求头。&lt;/p>
&lt;hr>
&lt;p>最近被安全同事拿着 Burp Suite 说我的服务有 CSRF 漏洞。&lt;/p>
&lt;p>我心想嘿这怎么可能，这种业界已经一大把通用方案的东西，我们直接一个 &lt;code>npm install csurf&lt;/code> 还能出什么差错？&lt;/p>
&lt;p>结果进 [csurf 的 GitHub 仓库]一看，居然 deprecated 了：&lt;/p>
&lt;blockquote>
&lt;p>This npm module is currently deprecated due to &lt;strong>the large influx of security vulunerability reports received&lt;/strong>, most of which are simply exploiting the underlying limitations of CSRF itself. The Express.js project does not have the resources to put into this module, which is largely unnecessary for modern SPA-based applications.&lt;/p>
&lt;/blockquote>
&lt;p>所以连这个周下载量最高峰时接近 60w 的 CSRF 中间件都有问题？&lt;/p>
&lt;p>这时突然意识到，自己对于 CSRF 攻击如何利用以及防御其实也只有浅浅的了解——大概也许或许就是别人构造一个隐藏表单 POST 过来就可以在用户不知情的情况下发起转账，而要防的话好像似乎直接 cookies 跟 input/header 搭配一下就完事儿了。那，CSRF 具体是怎么一回事儿呢？2023 年了，是更容易攻击还是更容易防御了？听说 Chrome 早就已经 SameSite=Lax by default 了，对 CSRF 防御有啥影响不？&lt;/p>
&lt;p>于是狠狠地花了一晚上的时间研究了一下。&lt;/p>
&lt;h2 id="如何利用">如何利用
&lt;/h2>&lt;blockquote>
&lt;p>Cross-Site Request Forgery (CSRF) is a type of attack that occurs when a malicious web site, email, blog, instant message, or program &lt;strong>causes a user&amp;rsquo;s web browser&lt;/strong> to perform an unwanted action on a trusted site when the user is authenticated.&lt;/p>
&lt;/blockquote>
&lt;p>反正就是通过各种手段，在用户不知情的情况下，让用户的浏览器往咱们的网站发了个请求，并且这个请求（往往）带有用户的登录态。&lt;/p>
&lt;p>这会造成什么情况呢？比如经典例子，如果银行转账的 API 接口是&lt;/p>
&lt;pre tabindex="0">&lt;code>GET https://www.teyvatbank.com/elemental-transactions/paimonSpecial?amount=2568Mora&amp;amp;currency=GenesisCrystals&amp;amp;payee=AdventurersGuild
&lt;/code>&lt;/pre>&lt;p>那我只需要构造一个网页，里边带上这个链接：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">a&lt;/span> &lt;span class="na">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;https://www.teyvatbank.com/elemental-transactions/paimonSpecial?amount=2568Mora&amp;amp;currency=GenesisCrystals&amp;amp;payee=AdventurersGuild&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>点击查看 OpenAI 员工联名信&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">a&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后把这个网页放到随便一个我可以控制的网站（成本相当低廉，不需要黑掉 &lt;a class="link" href="https://www.teyvatbank.com" target="_blank" rel="noopener"
>www.teyvatbank.com&lt;/a> 喔），通过社工手段诱导用户点击并在浏览器内打开——嚯，用户的 2568 Mora 就被转走了！&lt;/p>
&lt;p>之所以如此，是因为浏览器（往往）在发送请求时会带上对应站点的 Cookies，且 Cookies 内带有用户的登录态，于是网站就收到了一个看起来很正常的请求，并执行了对应的操作。就算这个请求实际上是从别的网站发起的。&lt;/p>
&lt;p>这只是一个简单的例子。接下来我们尝试站在攻击者的角度，看看如何能够发动此类攻击。&lt;em>暂时忽略 SameSite 的存在，哈哈。&lt;/em>&lt;/p>
&lt;h3 id="get">GET
&lt;/h3>&lt;p>如果目标接口是一个 GET 接口，那可以利用的方式数不胜数。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- 经典 a 标签 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">a&lt;/span> &lt;span class="na">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;adventurers&amp;#34;&lt;/span> &lt;span class="na">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;https://www.teyvatbank.com/elemental-transactions/paimonSpecial?amount=2568Mora&amp;amp;currency=GenesisCrystals&amp;amp;payee=AdventurersGuild&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>关注星瞳_Official&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">a&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- 甚至我可以自动点击？ --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getElementById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;adventurers&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">click&lt;/span>&lt;span class="p">()&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- img 图片静默加载 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">img&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;https://www.teyvatbank.com/elemental-transactions/paimonSpecial?amount=2568Mora&amp;amp;currency=GenesisCrystals&amp;amp;payee=AdventurersGuild&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- 让用户提交一个 form 表单 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">form&lt;/span> &lt;span class="na">action&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;https://www.teyvatbank.com/elemental-transactions/paimonSpecial?amount=2568Mora&amp;amp;currency=GenesisCrystals&amp;amp;payee=AdventurersGuild&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;submit&amp;#34;&lt;/span> &lt;span class="na">value&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;送溜溜梅&amp;#34;&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">form&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- 不演了，直接 JavaScript 发请求吧 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">endpoint&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;https://www.teyvatbank.com/elemental-transactions/paimonSpecial?amount=2568Mora&amp;amp;currency=GenesisCrystals&amp;amp;payee=AdventurersGuild&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">fetch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">endpoint&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">method&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;GET&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">mode&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;no-cors&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 由于这是一个 simple request，所以不会触发 preflight
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// 无论 Access-Controls-Allow-Credentials 如何设置都会直接请求喔
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// @see https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">credentials&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;include&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- 等你来补充。 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>所以，对于&lt;strong>会改变状态&lt;/strong>的接口，千万不要用 GET。这也不符合 GET 的语义。&lt;/p>
&lt;h3 id="post-w-x-www-form-urlencoded">POST w/ &lt;code>x-www-form-urlencoded&lt;/code>
&lt;/h3>&lt;p>假设我们的接口吸取教训，变成了人见人爱的 POST 接口：&lt;/p>
&lt;pre tabindex="0">&lt;code>POST https://www.teyvatbank.com/elemental-transactions/paimonSpecial
Content-Type: application/x-www-form-urlencoded
amount=2568Mora&amp;amp;currency=GenesisCrystals&amp;amp;payee=AdventurersGuild
&lt;/code>&lt;/pre>&lt;p>那可以利用的方式就少一些了。像 &lt;code>&amp;lt;a&amp;gt;&lt;/code> 标签、&lt;code>&amp;lt;img&amp;gt;&lt;/code> 标签等都无法直接发起 POST 请求。如果没有其他漏洞，通常可供利用的方式就只有 &lt;code>&amp;lt;form&amp;gt;&lt;/code> 和 XHR / Fetch API 请求。&lt;/p>
&lt;p>依葫芦画瓢，我们可以这样构造一个 &lt;code>&amp;lt;form&amp;gt;&lt;/code> 表单：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">form&lt;/span> &lt;span class="na">method&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;post&amp;#34;&lt;/span> &lt;span class="na">action&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;https://www.teyvatbank.com/elemental-transactions/paimonSpecial&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;hidden&amp;#34;&lt;/span> &lt;span class="na">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;amount&amp;#34;&lt;/span> &lt;span class="na">value&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;2568Mora&amp;#34;&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;hidden&amp;#34;&lt;/span> &lt;span class="na">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;currency&amp;#34;&lt;/span> &lt;span class="na">value&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;GenesisCrystals&amp;#34;&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;hidden&amp;#34;&lt;/span> &lt;span class="na">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;payee&amp;#34;&lt;/span> &lt;span class="na">value&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;AdventurersGuild&amp;#34;&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;submit&amp;#34;&lt;/span> &lt;span class="na">value&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;大的要来了&amp;#34;&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">form&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- 我也可以自动点击？ --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;form&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">click&lt;/span>&lt;span class="p">()&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>通过 XHR / Fetch API 也是一样的：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">endpoint&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;https://www.teyvatbank.com/elemental-transactions/paimonSpecial&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">fetch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">endpoint&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">method&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;POST&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">mode&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;no-cors&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">body&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;amount=2568Mora&amp;amp;currency=GenesisCrystals&amp;amp;payee=AdventurersGuild&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">headers&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;Content-Type&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;application/x-www-form-urlencoded&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 即使是 POST，即使设置了 Headers，这仍然是一个 simple request 捏
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// 复习一下：https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">credentials&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;include&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="reference">Reference
&lt;/h2>&lt;ul>
&lt;li>&lt;a class="link" href="https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html" target="_blank" rel="noopener"
>https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://security.stackexchange.com/questions/265447/can-we-set-a-custom-csrf-token-inside-header-requests-to-make-a-csrf-poc" target="_blank" rel="noopener"
>https://security.stackexchange.com/questions/265447/can-we-set-a-custom-csrf-token-inside-header-requests-to-make-a-csrf-poc&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://web.dev/articles/samesite-cookies-explained" target="_blank" rel="noopener"
>https://web.dev/articles/samesite-cookies-explained&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" target="_blank" rel="noopener"
>https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>在不使用 VPS 的情况下部署你的 Hugo 博客</title><link>https://rwong.tech/post/deploy-your-hugo-blog-without-vps/</link><pubDate>Wed, 13 Oct 2021 03:00:00 +0800</pubDate><guid>https://rwong.tech/post/deploy-your-hugo-blog-without-vps/</guid><description>&lt;h2 id="选型">选型
&lt;/h2>&lt;p>之前我们用 Caddy 的 http.git 插件来实现这样的效果：Git 仓库更新 -&amp;gt; 触发 Caddy 在本地拉取 Git 仓库 -&amp;gt; 使用 Hugo 生成网站 -&amp;gt; 用 Caddy 作为静态文件服务器 serve 博客。然后我们就得到了一个以 GitHub 仓库为 source 的静态博客。&lt;/p>
&lt;p>缺点在于，&lt;strong>你需要一个服务器&lt;/strong>来做这个事情。结果就是，你每个月都要花好几十块大洋去养一个超低负载的云服务器，计算资源被浪费的同时，你可能也得不到一个好的访问速度（比如 1 Mbps 小水管）。&lt;/p>
&lt;p>既然 Hugo 博客本质是一堆静态文件，于是我们可以考虑另外一种更快速也更省钱的静态文件托管方式：&lt;strong>对象存储 + CDN&lt;/strong>。对象存储的存储成本要比云服务器的硬盘低很多，你也不需要为云服务器的 CPU、内存、带宽等资源付费。如果对象存储的流量费用让你有些犹豫，你还可以搭配 CDN 服务，在提速的同时又能降费（是不是很神奇）。&lt;/p>
&lt;p>那没了计算资源，我们怎么跑 Hugo 呢？其实跑 Hugo 就是 CI/CD 的一种形式，于是我们可以用 GitHub Actions 不花一分钱来做这个事情（如果你的博客是 public repository 的话）。&lt;/p>
&lt;h2 id="准备对象存储与-cdn">准备对象存储与 CDN
&lt;/h2>&lt;p>这里我用的是腾讯云的对象存储（COS）和 CDN，你也可以随便选一个你喜欢的。具体过程不赘述，可以参考云服务提供商的文档，大概的思路是&lt;/p>
&lt;ol>
&lt;li>建一个存储桶，并且开启存储桶的网站访问能力&lt;/li>
&lt;li>建一个 CDN 加速域名，源站为存储桶&lt;/li>
&lt;li>建一个子用户（IAM 之类的），授权存储桶的读写权限，拿到 AccessKey 和 SecretKey&lt;/li>
&lt;li>配置 GitHub Actions&lt;/li>
&lt;/ol>
&lt;p>腾讯云 COS 有个坑点是，如果你想要用 CDN 搭配 COS 静态网站，那么你&lt;strong>不能&lt;/strong>打开 COS 静态网站的自动 HTTPS 功能，而应该在 CDN 处配置自动 HTTPS 跳转，否则会得到一个白屏。&lt;/p>
&lt;h2 id="配置-github-actions">配置 GitHub Actions
&lt;/h2>&lt;p>又到了 yaml 工程师最喜欢的环节，配置 GitHub Actions 实际上就是写 workflow 的 yaml 文件，告诉它当 XXX 发生时，需要做 YYY 和 ZZZ。这里需要注意的是，访问存储桶需要用到子用户的 AccessKey 和 SecretKey，这些东西直接写在 yaml 里自然是不行的，所以我们把它们先配置到仓库的 Secrets 里。&lt;/p>
&lt;p>这里有一份示例 yaml 文件，直接复制到你的仓库的 &lt;code>.github/workflows/deploy-to-cos.yaml&lt;/code> 并配置好 Secrets 就可以开启 Hugo 博客部署到腾讯云 COS 的自动化流程了。&lt;/p>
&lt;p>Secrets 列表：&lt;/p>
&lt;ul>
&lt;li>&lt;code>TENCENT_CLOUD_SECRET_ID&lt;/code>: 子用户的用户名，或者叫 AccessKey，或者叫 SecretID&lt;/li>
&lt;li>&lt;code>TENCENT_CLOUD_SECRET_KEY&lt;/code>: 子用户的密码，或者叫 SecretKey&lt;/li>
&lt;li>&lt;code>COS_BUCKET&lt;/code>: 存储桶的 ID，比如 &lt;code>rwong-tech-blog-xxxxxxxxxx&lt;/code> 之类的&lt;/li>
&lt;li>&lt;code>COS_REGION&lt;/code>: 存储桶的地域，比如 &lt;code>ap-guangzhou&lt;/code> 之类的&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deploy to COS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">push&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">main ]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pull_request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">main ]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workflow_dispatch&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">build-and-deploy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># The type of runner that the job will run on&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Steps represent a sequence of tasks that will be executed as part of the job&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/checkout@v2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">submodules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Setup Hugo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">peaceiris/actions-hugo@v2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hugo-version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;latest&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">extended&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">hugo --minify&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deploy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TencentCloud/cos-action@v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secret_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ secrets.TENCENT_CLOUD_SECRET_ID }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secret_key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ secrets.TENCENT_CLOUD_SECRET_KEY }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cos_bucket&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ secrets.COS_BUCKET }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cos_region&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ secrets.COS_REGION }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">local_path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">public&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remote_path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">clean&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="效果">效果
&lt;/h2>&lt;p>你看到的这个博客目前（2021 年 10 月）就是用这样的流程搭建的。&lt;/p>
&lt;p>得益于 GitHub Codespaces，博客文章的撰写也不需要再用到本地能力了，浏览器里直接就有一个现成的 VS Code，还可以跑 &lt;code>hugo server&lt;/code> 实时在线预览。撰写完成后 commit &amp;amp; push，就能将文章发布到网络上。&lt;/p>
&lt;h2 id="题外话">题外话
&lt;/h2>&lt;p>这篇文章的 slug 里说的是 &lt;code>without VPS&lt;/code>，虽说我们已经很少看见 &lt;code>VPS&lt;/code> 这个词语了，取而代之的是&lt;strong>云服务器&lt;/strong>，什么 &lt;code>EC2&lt;/code>, &lt;code>ECS&lt;/code>, &lt;code>CVM&lt;/code> 云云，但我们也都清楚，这些都是高级一点的 &lt;code>VPS&lt;/code> 罢了。&lt;/p>
&lt;p>当然他们的底层实现和以前的 VPS 还是有很大不同的，部署在云环境里的云服务器通常都有更灵活的资源升降配，具备在宿主机间快速转移的能力，多台主机之间也能组成云上的私有网络，规模上来之后成本可以降低不少。&lt;/p></description></item><item><title>使用 IPv6 + WireGuard 实现三 网 融 合</title><link>https://rwong.tech/post/access-lan-from-remote-with-ipv6/</link><pubDate>Wed, 19 Feb 2020 23:00:00 +0800</pubDate><guid>https://rwong.tech/post/access-lan-from-remote-with-ipv6/</guid><description>&lt;p>先声明一下，本文介绍的内容均用于合法合规的用途。可能学校网络中心不太乐意大家自建隧道，因为这实质上允许了未经许可的外网访问，所以如果搭建了的话还请自用，不要分享给校外人员使用权。&lt;/p>
&lt;h2 id="背景">背景
&lt;/h2>&lt;p>由于学校校园网 VPN 并发数只有 1，所以如果在家里有多台设备需要同时远程连入校园网的需求时就比较难办。起初是没有这种需求的，因为一般只需要手上这台性能一般的电脑连入校园网，再远程访问到我放在实验室里的另一台性能强劲的电脑，就可以做几乎所有的事情。&lt;/p>
&lt;p>但是恰好在开发一个需要校园网的小程序，所以手机也需要连上校园网，而电脑也需要远程到校园网通过另一台电脑里操作微信开发者工具。于是问题就来了。&lt;/p>
&lt;p>我曾经试过在一台 Windows 设备上开 AnyConnect 然后再开移动热点共享给其它设备，结果发现它卡在初始化移动热点上，不动了。后来我重启了一下电脑，先开移动热点后连 AnyConnect，这次倒是成功开启了，可是移动设备连上热点后并没有连入校园网。&lt;/p>
&lt;p>还有另外一种方案。由于家里有路由器，而且刷了 Padavan，所以其实可以在路由器上安装 OpenConnect，这样路由器后边的设备都可以共享这个连接。但是，由于校园网 VPN 下发的路由里将所有的 IPv6 都导向了 VPN，所以家里的 IPv6 访问速度会从 150Mbps 下降到不到 30Mbps，若每次手动修改路由表又显得十分麻烦。&lt;/p>
&lt;p>更重要的是，校园网 VPN 的带宽是有限的。而像我这种远程桌面重度用户，显然会挤占其他优先级更高的学术流量。那么答案很明显了——自建隧道“连入校园网”。&lt;/p>
&lt;h2 id="连连看">连连看
&lt;/h2>&lt;p>那么怎么自建隧道呢？隧道隧道，总是要有两端的，而且这两端要能够直接建立连接。放在我们的场景里很明显，一端在家里，一端在校园网里。于是我们可以用家里的路由器作为一端，以校园网里我们能够控制的一台设备作为另一端（也就是我放在实验室的电脑了）。&lt;/p>
&lt;p>现在人在家里，第一个需要解决的问题就是如何连上校园网里的这台设备。那你就会问了，我要连上校园网里的这台设备，就需要连上校园网；而我要连上校园网，又得连上这台校园网里的设备，经典的 bootstrap 问题怎么解？&lt;/p>
&lt;p>其实这个问题的前半部分是不成立的。因为这个“连”，它可以不是主动的。&lt;/p>
&lt;p>每台连入校园网的设备都会获得一个 v4 的 IP 地址和 v6 的 IP 地址。以我放在实验室的 Windows 设备为例，连入有线校园网后得到的 IP 为 &lt;code>172.18.xxx.xxx&lt;/code> 和 &lt;code>2001:250:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx&lt;/code>。&lt;/p>
&lt;p>我们来讨论一下 IPv4 的情况。&lt;code>172.18.xxx.xxx&lt;/code>是内网地址，我们没法直接连上，需要“连上校园网”后才可以。这个设备经过校园网的对称型 NAT 网关后在公网上表现为 &lt;code>120.xxx.xxx.xxx&lt;/code>，而显然，我们在家里去连这个 &lt;code>120.xxx.xxx.xxx&lt;/code> 也是不会有结果的，更何况电信给我们分配的 IP 也是 &lt;code>100.64.xxx.xxx&lt;/code> 的内网 IP。两个 NAT 后边的设备想要互访，有没有可能呢？有，除非双方都是对称型 NAT。好在电信的 NAT（疑似）是完全锥型 NAT，所以通过 UDP 打洞的方式是可以连上的。&lt;/p>
&lt;p>故首先使用了 ZeroTier 的方案，借助官方的 Moon 确实可以实现互访，整个设置方法十分便捷。两边的设备输入一个 ID，然后分配一下 IP 就可以了。但是在实际使用的过程中（例如连接校内设备的远程桌面），大约十分钟就断 线一次，且需要很长一段时间才能恢复连接。我个人猜测是 NAT UDP 打洞不稳定（比如映射关系会一段时间后变化？），所以弃用了该方案。&lt;/p>
&lt;p>IPv6 的世界没有 NAT，但是这也不代表我们就能直接连上 &lt;code>2001:250:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx&lt;/code> 了。校园网边界防火墙的存在，使得默认情况下校园网内的设备是完全无法接受 IPv6 传入连接的。怎么办？禁止了传入连接并不代表我们无法向对方发送数据，只要对方向我们请求连接，那么之后与这条连接有关的数据包（不管是来的还是去的）就可以被防火墙放行。比如 iptables 里很常见的一条：&lt;/p>
&lt;pre tabindex="0">&lt;code># iptables -L
Chain INPUT (policy DROP)
target prot opt source destination
ACCEPT all -- anywhere anywhere state RELATED,ESTABLISHED
&lt;/code>&lt;/pre>&lt;p>那对方能向我们请求连接么？由于电信并不像校园网那样给我们做安 全 防 护，所以我们家里的路由器本身是可以接受传入连接的。&lt;/p>
&lt;p>那么这就开搞了！&lt;/p>
&lt;h2 id="现代的隧道方案">现代的隧道方案
&lt;/h2>&lt;p>传统的 PPTP、L2TP 就不说了，虽然操作系统有原生支持，但是好像不支持下发路由（？没了解过），而且运营商似乎有限制之类的，连学校都已经弃用了。&lt;/p>
&lt;p>前后试用过 SoftEther 与 OpenVPN。其中前者不支持下发路由（需要购买商业版才可以）；后者效果挺可，流畅使用了一下午后，由于 Padavan 给 Windows 设备下发的 IPv6 地址动不动就自己没掉，所以我就把 Padavan 换成 OpenWRT 了。这一换，原本 Padavan 原生提供的很好用的 OpenVPN 管理功能就没了，OpenWRT 提供的 luci-app-openvpn-server 就简陋很多，看起来是为了免流而设计的（自带特殊代码&amp;hellip;）。简单试用之后，觉得有点奇怪，好像少了生成证书的功能，后来才发现是自带证书&amp;hellip;&amp;hellip;安全性不保。&lt;/p>
&lt;p>后来白白推荐了 WireGuard。官网宣传上说是 Fast, Modern, Secure。emmmmm配起来似乎确实挺简单的（相比于 OpenVPN 来说），但是也没那么傻瓜，Windows 上的图形界面就是给你一个配置文件的编辑器（&lt;/p>
&lt;p>WireGuard 与之前我试用过的这些隧道软件不同，它没有服务器和客户端的概念，每个节点都是“平等的” Peer（也就是 C/S 变成了 P2P）。每个 Peer 会有一个 PublicKey 与 PrivateKey，通过 &lt;code>wg genkey&lt;/code> 可以直接生成，然后填入配置文件即可。&lt;/p>
&lt;p>来配置。首先确定子网，通常我们可以随便挑一个不冲突的 /24 段，比如 &lt;code>192.168.2.0/24&lt;/code> 之类。这里由于某些原因，我挑了一个稍显奇怪的 /29 段。&lt;/p>
&lt;p>OpenWRT 这端我用了自带 WireGuard 的固件，具体操作就是 Linux 端的 luci 复刻版：在网络接口里新建一个类型为 WireGuard 的接口（就叫 wg0 好了），然后填入PrivateKey、监听端口，以及我们要给接口分配的 IP（如 192.168.124.1）。注意这里是给接口分配的 IP，要以 CIDR 格式写上，才能让路由器知道你和哪些设备在同一个局域网。这里我填的是 &lt;code>192.168.208.99/29&lt;/code>，意思是我的 IP 是 &lt;code>192.168.208.99&lt;/code>，而且是处于一个 /29 的网段内。&lt;/p>
&lt;p>之后设置对端 Peer 的 PublicKey、对端 Peer 允许的 IP 列表（也就是，要设置什么路由走向这个 Peer，通常都会包含我们给这个 Peer 分配的 IP，以及我们希望从那个 Peer 那里访问到什么网段）。在初步测试的情况下，我们只填对端 IP：&lt;code>192.168.208.97/32&lt;/code>。嘿注意，这里我们写的是 &lt;code>/32&lt;/code>，因为这里我们实际上写的是路由表项。我们只希望发往 &lt;code>192.168.208.97/32&lt;/code> 的流量流向这个 Peer。&lt;/p>
&lt;p>保存应用设置，重启一下这个接口，差不多就搞定了。&lt;/p>
&lt;p>&lt;img src="https://cdn.rwong.tech/blog/20200219155424.png"
loading="lazy"
alt="20200219155424.png"
>&lt;/p>
&lt;p>顺带一提，防火墙区域最好选择 wan；Peer 设置里勾选上“路由允许的 IP”，Endpoint 不填。&lt;/p>
&lt;p>&lt;img src="https://cdn.rwong.tech/blog/20200219164508.png"
loading="lazy"
alt="20200219164508.png"
>&lt;/p>
&lt;p>在另一端呢，使用 Windows 客户端，手动新建一个 Empty Tunnel，然后键入配置文件。配置文件的内容与刚才在 OpenWRT，同时在添加 Peer 时加上 Endpoint，也就是我们 OpenWRT 这端的地址。&lt;/p>
&lt;p>&lt;img src="https://cdn.rwong.tech/blog/20200219160121.png"
loading="lazy"
alt="20200219160121.png"
>&lt;/p>
&lt;p>这就是玄妙之处了：让校园网里的对端主动连上我们，所以我们不在我们这端填对端 Peer 的 Endpoint，而是在对端那边填上我们这端的 Endpoint，这样就能让对端向我们主动建立连接。&lt;/p>
&lt;p>之后两端都激活之后，根据我们在 AllowedIPs 里的设置，两端都可以用对端设置的 IP 来 ping 通对端。&lt;/p>
&lt;p>&lt;img src="https://cdn.rwong.tech/blog/20200219165403.png"
loading="lazy"
alt="20200219165403.png"
>&lt;/p>
&lt;p>由于路由器里加入了 wan 区域，所以我们不仅可以在路由器上 ping 对端，还可以在路由器后边的设备上 ping 对端。&lt;/p>
&lt;p>&lt;img src="https://cdn.rwong.tech/blog/20200219165939.png"
loading="lazy"
alt="20200219165939.png"
>&lt;/p>
&lt;h2 id="开始融合">开始融合
&lt;/h2>&lt;p>现在我们已经成功建立了连接，两台机器看起来就像是在同一个局域网里一样（尽管实际上中间隔了不知道多少台路由器）。接下来的任务，就是要能够通过校园网里的这台设备，去访问校园网里的其它设备。&lt;/p>
&lt;p>How it works? 我们只需要让发往校园网里其他设备的网络包，都发送到那台设备去，然后让那台设备代为转发；而为了能够“一来一回”，我们需要那台设备充当一个 NAT 网关的角色（毕竟我们这个自建的隧道并不会给我们家里的设备分配校园网 IP）。说了这么多，其实就是要让校园网里的那台设备，变成一台路由器。&lt;/p>
&lt;p>那怎么让它变成一台路由器呢？&lt;/p>
&lt;p>这台设备安装的是 Windows 10 Pro 系统。我在网上找了很久，只找到说要启用一个 RRAS 服务（Routing and Remote Access Service）。我就试着启用了一下，嘿，还真能转发包了。&lt;/p>
&lt;p>不过后来我没有采用这种方案，原因待会儿再说。好的，那一旦我们这台设备变成路由器以后，我们就可以再配置一下 WireGuard，使得家里这边发往校园网其它设备的包，都先发送到隧道对端去。&lt;/p>
&lt;p>配置很简单，只需要往 AllowedIPs 加入我们期望的路由表项即可。例如 &lt;code>10.0.0.0/8&lt;/code>, &lt;code>172.16.0.0/12&lt;/code> 之类。由于我们勾选了“路由允许的 IP”，所以从路由器上发往这些地址的包，都会从隧道走。&lt;/p>
&lt;p>&lt;img src="https://cdn.rwong.tech/blog/20200219204236.png"
loading="lazy"
alt="20200219204236.png"
>&lt;/p>
&lt;p>然后你就能愉快地从路由器后边的设备畅享校园网了！&lt;/p>
&lt;p>&lt;img src="https://cdn.rwong.tech/blog/20200219204456.png"
loading="lazy"
alt="20200219204456.png"
>&lt;/p>
&lt;h2 id="三网融合">三网融合
&lt;/h2>&lt;p>人们总是喜新厌旧，期望着更好的。&lt;/p>
&lt;p>为了更加便于远程办公，我又有一个新需求：远程访问另一个内网。这个内网部署在校园网内，也是前边有个路由器，占用网段 &lt;code>192.168.1.0/24&lt;/code>。可惜没有 IPv6。&lt;/p>
&lt;p>在网上搜索到一篇文章：&lt;a class="link" href="https://anyisalin.github.io/2018/11/21/fast-flexible-nat-to-nat-vpn-wireguard/" target="_blank" rel="noopener"
>通过 Wireguard 构建 NAT to NAT VPN&lt;/a>，刚好就可以解决我们的问题。正由于 WireGuard 这种 P2P 的设计，让这种 NAT to NAT VPN 变得十分易于部署。&lt;/p>
&lt;p>以我放在校园网里的那台设备为 Gateway（因为它能访问到家里，它也能被那个内网所访问），之后的事情就是在 &lt;code>192.168.1.0/24&lt;/code> 内网里挑一台设备作为 WireGuard Peer（分配地址 &lt;code>192.168.208.98&lt;/code>），并加入到 Gateway 的 Peer 列表里。&lt;/p>
&lt;p>事情很顺利，直到我从 &lt;code>192.168.208.98&lt;/code> Ping 回家中的 &lt;code>192.168.208.99&lt;/code> 时：&lt;/p>
&lt;p>&lt;img src="https://cdn.rwong.tech/blog/20200219205601.png"
loading="lazy"
alt="20200219205601.png"
>&lt;/p>
&lt;p>这就很奇怪了。意思好像是说，我应该换个网关？&lt;/p>
&lt;p>之前在找 Windows 设备如何配置成为一台路由器时，大家都说要用 Windows Server。虽然误打误撞开启了 Windows 10 Pro 里的 RRAS，但是我没有任何办法去配置它。Windows Server 同样也是开启这个服务，不过它有一个服务器控制台可以控制相关的 NAT 与路由设置。后来好不容易得知 Windows 10 也可以下载相关组件来远程管理 Windows Server 上的这些服务器功能，装完之后发现这个组件确实检测到了本机，但是又写了一句“非服务器计算机”。同样没有给出任何操作面板。&lt;/p>
&lt;p>&lt;img src="https://cdn.rwong.tech/blog/20200219210216.png"
loading="lazy"
alt="20200219210216.png"
>&lt;/p>
&lt;p>虽然似乎这个 &lt;code>Redirect Network&lt;/code> 的提示不影响网络访问，但我还是怕这个无法自行配置的 RRAS 会对网络造成什么影响（还是懂得太少造成的安全感缺失）。于是在本机上开了个 Linux 虚拟机 + 桥接网卡，来作为整个组网方案中的 Gateway。&lt;/p>
&lt;p>为了尽量降低内存占用，这边采用 Alpine Linux 作为虚拟机的系统。Alpine Linux 官方有个 Wiki 页面介绍如何配置 WireGuard，但其实用 &lt;code>wg-quick&lt;/code> 会比 Wiki 介绍的手动配置方法更便捷。我们只需要把之前 Windows 版的配置文件原样复制到 &lt;code>/etc/wireguard/wg0.conf&lt;/code>，之后 &lt;code>wg-quick up wg0&lt;/code> 就可以了。&lt;/p>
&lt;p>当然还要记得设置 &lt;code>net.ipv4.ip_forward = 1&lt;/code>，并且 iptables 配置为允许 FORWARD 相关的 packet。&lt;/p>
&lt;p>而为了实现三网融合，我们需要在每台 peer（包括 Gateway）设置正确的 AllowedIPs。假设：&lt;/p>
&lt;ul>
&lt;li>Peer 1 在 &lt;code>192.168.1.0/24&lt;/code> 中&lt;/li>
&lt;li>Gateway 在 &lt;code>172.18.187.0/24&lt;/code> 中&lt;/li>
&lt;li>Peer 2 在 &lt;code>192.168.123.0/24&lt;/code> 中&lt;/li>
&lt;/ul>
&lt;p>若是三个网段能够完完全全的互访，那么&lt;/p>
&lt;ul>
&lt;li>Peer 1 给 Gateway 设置的 AllowedIPs 应该为 &lt;code>192.168.208.96/29, 192.168.123.0/24&lt;/code>（为啥不加 &lt;code>172.18.187.0/24&lt;/code> 呢？因为本来就可以访问）&lt;/li>
&lt;li>Gateway 给 Peer 1 设置的 AllowedIPs 应该为 &lt;code>192.168.208.98/32, 192.168.1.0/24&lt;/code>&lt;/li>
&lt;li>Gateway 给 Peer 2 设置的 AllowedIPs 应该为 &lt;code>192.168.208.99/32, 192.168.123.0/24&lt;/code>&lt;/li>
&lt;li>Peer 2 给 Gateway 设置的 AllowedIPs 应该为 &lt;code>192.168.208.96/29, 192.168.1.0/24&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>这样就大功告成了。每个 Peer 只需要添加 Gateway 作为他们的唯一 Peer，并且设置其 AllowedIPs 为 WireGuard 局域网网段 + 他们想要访问的网段即可；而 Gateway 需要添加所有的 Peer，并且设置相应的 AllowedIPs 为 Peer 在 WireGuard 局域网网段的 IP + Peer 所在的 NAT 网段。&lt;/p>
&lt;p>而由于我不需要完完全全的互访，比如我不需要从 Peer 1 访问 Peer 2 所在的网段，那我就不需要在 Peer 1 和 Gateway 上配置 &lt;code>192.168.123.0/24&lt;/code>。&lt;/p>
&lt;h2 id="告一段落">告一段落？
&lt;/h2>&lt;p>正当我想要愉快地使用自建隧道跑远程桌面时，我就发现这个隧道，好卡。&lt;/p>
&lt;p>打开任务管理器 + 疯狂拖动窗口（这样可以瞬间提高视频码率至最高），看网络吞吐量，最多也就只有 3 Mbps。而平日在学校大内网，这个速度至少也有 30 Mbps。&lt;/p>
&lt;p>那我就不用远程桌面好了，VSCode Remote 它不香么？之后我又一通操作，配好 SSH Remote，准备开始调试 Angular 应用。结果一打开，豁，白屏————————好久。&lt;/p>
&lt;p>开 F12 看看，噢原来一直在下载 js 脚本。咱的资源大概有 2~30 MB，下载得好一会儿呢。算了，初次加载慢一点就慢一点，不能忍的是，它居然下载到一半就网络错误中断了！回头看 VSCode，远程终端无响应，过了一会儿自动重连了。&lt;/p>
&lt;p>怎么会这么菜呢？难道上传带宽就这么点？而且还这么不稳定？我又跑去 &lt;a class="link" href="https://speed.neu6.edu.cn/" target="_blank" rel="noopener"
>东北大学测速网站&lt;/a> 跑了一下测速，结果 IPv6 上传明明有 10 Mbps 呀！&lt;/p>
&lt;p>然后我又在家里跑了校园网大文件测速，结果发现下载速度还是有 600 KB/s 左右，也没有出现中断的情况。虽然速度抖动的很厉害，但是这就显得远程桌面的 3 Mbps 吞吐量与 SSH 断连更加诡异了。&lt;/p>
&lt;p>后来想了想，会不会是 UDP 的锅？或者更准确地说，是运营商 QoS 对 UDP （尤其是跨网的）限速了。不过我还不太能确认，毕竟跑大文件下载的速度和远程桌面的速度存在这么大的差异，还是十分令人匪夷所思。&lt;/p>
&lt;p>WireGuard 使用 UDP 协议，且官方没有加入 TCP 支持。有没有什么办法用 TCP 呢？&lt;/p>
&lt;p>OpenVPN 是支持 TCP 的，但是配置三网融合很麻烦（虽然如果速度太慢的话，三网融合也没有什么意义）。事实上，我确实又用 OpenVPN 配了个两点间的隧道，远程桌面的吞吐量可喜地上去了，可是 ping 和 jitter 却搞得飞起，原本 40+ ms 变成 80+ ms，不知为何。&lt;/p>
&lt;p>突然想起以前在主机圈看过一些人对付 UDP QoS 的骚操作：伪装。&lt;/p>
&lt;p>隆重介绍我们的好朋友：&lt;a class="link" href="https://github.com/wangyu-/udp2raw-tunnel" target="_blank" rel="noopener"
>udp2raw-tunnel&lt;/a>，一个可以让 UDP 伪装成 TCP 甚至 ICMP 的隧道。&lt;/p>
&lt;p>整个部署很简单，OpenWRT 上也有相应的 luci 包可以简化你的配置。在我这里只需要优化 Gateway 到 Peer 2 的链路就可以了，又由于 Gateway 处于主动的位置，所以在 Gateway 部署 udp2raw client，在 OpenWRT 上部署 udp2raw server 即可。然后我们设定一下 udp2raw，local 就是要监听的地址:端口，remote 就是要把包转发出去的地址:端口，这些 README 中都有相应介绍。&lt;/p>
&lt;p>不过需要注意的是，由于 OpenWRT 的 luci 包默认将 udp2raw 运行在 client 模式，所以需要手动修改一下 &lt;code>/etc/init.d/udp2raw&lt;/code>，将其中写入配置文件的地方里的 &lt;code>-c&lt;/code> 改成 &lt;code>-s&lt;/code>。此外，还需要更改 WireGuard 配置，将 MTU 调为 1280（最低值），避免 udp2raw 出现 huge packet 警告。&lt;/p>
&lt;p>再试试？重新建立连接后，打开远程桌面，豁，丝般流畅，媲美（不卡的时候的）校网 VPN。&lt;/p>
&lt;p>这个伪装会不会有点不道德？我觉得 udpspeeder 这种多倍发包的比较不道德，毕竟会更劣化网络质量。我们这种伪装，应该还好，毕竟我们的实际体验也证实我们无法接受那种程度的丢包。&lt;/p>
&lt;h2 id="还有一件事">还有一件事
&lt;/h2>&lt;p>我们的家宽每两天自动重拨号一次。&lt;/p>
&lt;p>这一拨号不要紧，路由器拿到的 IPv6 地址就变了呀！这也意味着，每两天我就得上一次路由器后台，复制一下地址，连一下校网 VPN，更改 Gateway 上 Peer 的 Endpoint。&lt;/p>
&lt;p>这种重复低效的劳动是应该避免的。解决问题的思路还是比较成熟的，那就是 DDNS。不过有两个问题：&lt;/p>
&lt;ul>
&lt;li>DNS 有 TTL，更新一个记录需要 TTL 之后才能生效，我在阿里云解析上最低能够设置为 10 分钟。不过更糟糕的是，我们的 DNS 服务器未必会遵守 TTL。&lt;/li>
&lt;li>WireGuard 在握手失败时，不会尝试重新解析域名。&lt;/li>
&lt;/ul>
&lt;p>在我们使用了 udp2raw 后，第二个问题迎刃而解，因为我们只需要让 udp2raw 更换远端地址就可以了。可惜，udp2raw 不支持 IPv6 域名，只能纯地址。&lt;/p>
&lt;p>那我们就不用传统 DDNS 方案了，不如自己实现一个？&lt;/p>
&lt;p>服务端部署在阿里云学生机上：&lt;a class="link" href="https://github.com/robinWongM/fast-ddns-server" target="_blank" rel="noopener"
>https://github.com/robinWongM/fast-ddns-server&lt;/a>&lt;/p>
&lt;p>客户端是一个简易 Python 脚本，负责在地址变动时重启 udp2raw。服务端在写的时候是支持多域名的，但客户端比较简单粗暴，只取拿到的第一条记录；且没有断线重连支持（似乎）。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="ch">#!/usr/bin/env python3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">websocket&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">json&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">subprocess&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">signal&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">SIGINT&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">peer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;YOUR PEER PUBLIC KEY&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">udp2raw&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">stop_udp2raw&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">udp2raw&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">udp2raw&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">terminate&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">udp2raw&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">wait&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Killed udp2raw&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">restart_udp2raw&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">endpoint&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">global&lt;/span> &lt;span class="n">udp2raw&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">stop_udp2raw&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">udp2raw&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">subprocess&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Popen&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s2">&amp;#34;udp2raw&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;-c&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;-a&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;-l&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;127.0.0.1:LISTEN_PORT&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;-r&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;[&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">endpoint&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">]:ENDPOINT_PORT&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;--raw-mode&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;faketcp&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;--cipher-mode&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;xor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;--auth-mode&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;md5&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;--retry-on-error&amp;#34;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Started udp2raw&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">on_message&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ws&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">message&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">msg&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loads&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">msg&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;event&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;list&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">entry&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">msg&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;data&amp;#39;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">msg&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;event&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;update&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">entry&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">msg&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;data&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Unknown message occurred: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Updating&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">restart_udp2raw&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">entry&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;value&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="ne">OSError&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Execution failed:&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">file&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stderr&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Updated: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">entry&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;value&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">on_error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ws&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">error&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">error&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">on_close&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ws&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;### closed ###&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">on_open&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ws&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ws&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">send&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="s1">&amp;#39;event&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;list&amp;#39;&lt;/span>&lt;span class="p">}))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="vm">__name__&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;__main__&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ws&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">websocket&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">WebSocketApp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;ws://SERVER_ADDRESS/subscribe&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">on_message&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">on_message&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">on_error&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">on_error&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">on_close&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">on_close&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ws&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">on_open&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">on_open&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ws&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">run_forever&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">stop_udp2raw&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>配合 &lt;code>init.d&lt;/code> 脚本食用，works like a charm。&lt;/p>
&lt;p>其实一开始写的是个 Windows 客户端，给 Windows 用，原理是动态修改 hosts 文件，支持 WebSocket 断线重连等高级功能。但是后来发现 WireGuard 在断线重连时不会重解析，遂作罢了。&lt;/p></description></item><item><title>使用 Caddy http.git 自动部署 Hugo 博客仓库</title><link>https://rwong.tech/post/setup-hugo-with-caddy-git/</link><pubDate>Fri, 31 Jan 2020 17:15:00 +0800</pubDate><guid>https://rwong.tech/post/setup-hugo-with-caddy-git/</guid><description>&lt;p>本来是想用 Ghost 的（不知道是因为喜欢它的 Node 技术栈，还是因为觉得 WordPress 太笨重/古老），但是那个编辑器对 CJK 用户太不友好了&amp;hellip;这个问题从 2018 年到现在&lt;a class="link" href="https://github.com/TryGhost/Ghost/issues/9801" target="_blank" rel="noopener"
>都没有解决&lt;/a>。虽说就在最近，有人研究了&lt;a class="link" href="https://soulteary.com/2020/01/19/bugfix-for-ghost-editor-cjk-input.html" target="_blank" rel="noopener"
>修复的方法&lt;/a>，但是又要用 Docker 的 bind mount，而我懒得改了（之前 Ghost 部署的时候直接用的 Volume），而且又不知道每次 Ghost 更新后对应的资源文件会不会有变动，整个维护成本变得相当大。Say Goodbye to Ghost👋&lt;/p>
&lt;p>于是打算在 GitHub 上建一个仓库存放博客内容（&lt;code>.md&lt;/code> 等），然后国内服务器上运行 Caddy 和 Hugo，当仓库更新时自动 Pull 并生成页面。省去每次手工部署的麻烦。&lt;/p>
&lt;p>记录一下部署过程。&lt;/p>
&lt;h2 id="服务器环境">服务器环境
&lt;/h2>&lt;p>阿里云&lt;strong>安 全 加 固&lt;/strong>版 Ubuntu Server 18.04.3 LTS&lt;/p>
&lt;h3 id="安装-go">安装 Go
&lt;/h3>&lt;p>因为不想自己手动 cp 到 /usr/local/bin，所以用了 PPA。来源：https://github.com/golang/go/wiki/Ubuntu&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">sudo add-apt-repository ppa:longsleep/golang-backports
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo apt update &lt;span class="c1"># 我觉得是多余的&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo apt install golang-go
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>因为没有 &lt;code>add-apt-repository&lt;/code>，所以还得&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">sudo apt install software-properties-common
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>配置一下 GOPROXY。（来源：https://segmentfault.com/a/1190000020293616）&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GO111MODULE&lt;/span>&lt;span class="o">=&lt;/span>on
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">go env -w &lt;span class="nv">GOPROXY&lt;/span>&lt;span class="o">=&lt;/span>https://goproxy.cn,direct
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="安装-caddy">安装 Caddy
&lt;/h2>&lt;p>由于需要一些官方网站上没有列出来的插件（如&lt;code>caddy-alidns&lt;/code>），所以需要自己手动 build Caddy。来源：https://github.com/caddyserver/caddy/wiki/Plugging-in-Plugins-Yourself&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">mkdir caddy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> caddy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">nano caddy.go &lt;span class="c1"># 见下方 caddy.go 内容&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">go mod init
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">go build &lt;span class="c1"># 之后当前目录下会有一个 caddy 文件&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>caddy.go&lt;/code> 文件内容：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">package&lt;/span> &lt;span class="nx">main&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;github.com/caddyserver/caddy/caddy/caddymain&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// plug in plugins here, for example:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// _ &amp;#34;import/path/here&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">_&lt;/span> &lt;span class="s">&amp;#34;github.com/colachg/caddy-alidns&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// hook.service 插件不是必要的
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">_&lt;/span> &lt;span class="s">&amp;#34;github.com/hacdias/caddy-service&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 下面这个插件似乎有点问题，build 不过去，所以取消掉了
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// _ &amp;#34;github.com/pieterlouw/caddy-net/caddynet&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">_&lt;/span> &lt;span class="s">&amp;#34;github.com/captncraig/caddy-realip&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">_&lt;/span> &lt;span class="s">&amp;#34;github.com/abiosoft/caddy-git&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">_&lt;/span> &lt;span class="s">&amp;#34;github.com/epicagency/caddy-expires&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">_&lt;/span> &lt;span class="s">&amp;#34;github.com/caddyserver/dnsproviders/cloudflare&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// optional: disable telemetry
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// caddymain.EnableTelemetry = false
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">caddymain&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Run&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后加入 systemd 豪华大套餐。&lt;/p>
&lt;p>来源：&lt;/p>
&lt;ul>
&lt;li>&lt;a class="link" href="https://github.com/caddyserver/caddy/wiki/Caddy-as-a-service-examples" target="_blank" rel="noopener"
>https://github.com/caddyserver/caddy/wiki/Caddy-as-a-service-examples&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://github.com/caddyserver/caddy/tree/master/dist/init/linux-systemd" target="_blank" rel="noopener"
>https://github.com/caddyserver/caddy/tree/master/dist/init/linux-systemd&lt;/a>&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 复制 caddy 文件到 /usr/local/bin + 处理权限&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo cp caddy /usr/local/bin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chown root:root /usr/local/bin/caddy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chmod &lt;span class="m">755&lt;/span> /usr/local/bin/caddy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 让非 root 权限下的 caddy 也可以监听 80, 443&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo setcap &lt;span class="s1">&amp;#39;cap_net_bind_service=+ep&amp;#39;&lt;/span> /usr/local/bin/caddy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># www-data 用户&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo groupadd -g &lt;span class="m">33&lt;/span> www-data
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo useradd &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -g www-data --no-user-group &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --home-dir /var/www --no-create-home &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --shell /usr/sbin/nologin &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --system --uid &lt;span class="m">33&lt;/span> www-data
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在 /etc 下存放配置与 SSL 证书&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo mkdir /etc/caddy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chown -R root:root /etc/caddy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo mkdir /etc/ssl/caddy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chown -R root:www-data /etc/ssl/caddy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chmod &lt;span class="m">0770&lt;/span> /etc/ssl/caddy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Caddyfile&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo touch /etc/caddy/Caddyfile
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chown root:root /etc/caddy/Caddyfile
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chmod &lt;span class="m">644&lt;/span> /etc/caddy/Caddyfile
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># /var/www 存放网页&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo mkdir /var/www
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chown www-data:www-data /var/www
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chmod &lt;span class="m">555&lt;/span> /var/www
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># systemd service unit&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">wget https://raw.githubusercontent.com/caddyserver/caddy/master/dist/init/linux-systemd/caddy.service &lt;span class="c1"># 可能下载不下来，这时得手动新建一下，文件内容见后&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo cp caddy.service /etc/systemd/system/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chown root:root /etc/systemd/system/caddy.service
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chmod &lt;span class="m">644&lt;/span> /etc/systemd/system/caddy.service
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo systemctl daemon-reload
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo systemctl start caddy.service
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 开机自启&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo systemctl &lt;span class="nb">enable&lt;/span> caddy.service
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>caddy.service&lt;/code> 文件内容：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-plaintext" data-lang="plaintext">&lt;span class="line">&lt;span class="cl">[Unit]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Description=Caddy HTTP/2 web server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Documentation=https://caddyserver.com/docs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">After=network-online.target
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Wants=network-online.target systemd-networkd-wait-online.service
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">; Do not allow the process to be restarted in a tight loop. If the
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">; process fails to start, something critical needs to be fixed.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">StartLimitIntervalSec=14400
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">StartLimitBurst=10
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[Service]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Restart=on-abnormal
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">; User and group the process will run as.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">User=www-data
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Group=www-data
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">; Letsencrypt-issued certificates will be written to this directory.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Environment=CADDYPATH=/etc/ssl/caddy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">; Always set &amp;#34;-root&amp;#34; to something safe in case it gets forgotten in the Caddyfile.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ExecStart=/usr/local/bin/caddy -log stdout -log-timestamps=false -agree=true -conf=/etc/caddy/Caddyfile -root=/var/tmp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ExecReload=/bin/kill -USR1 $MAINPID
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">; Use graceful shutdown with a reasonable timeout
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">KillMode=mixed
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">KillSignal=SIGQUIT
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">TimeoutStopSec=5s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">; Limit the number of file descriptors; see `man systemd.exec` for more limit settings.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">LimitNOFILE=1048576
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">; Unmodified caddy is not expected to use more than that.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">LimitNPROC=512
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">; Use private /tmp and /var/tmp, which are discarded after caddy stops.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PrivateTmp=true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">; Use a minimal /dev (May bring additional security if switched to &amp;#39;true&amp;#39;, but it may not work on Raspberry Pi&amp;#39;s or other devices, so it has been disabled in this dist.)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PrivateDevices=false
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">; Hide /home, /root, and /run/user. Nobody will steal your SSH-keys.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ProtectHome=true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">; Make /usr, /boot, /etc and possibly some more folders read-only.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ProtectSystem=full
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">; … except /etc/ssl/caddy, because we want Letsencrypt-certificates there.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">; This merely retains r/w access rights, it does not add any new. Must still be writable on the host!
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ReadWritePaths=/etc/ssl/caddy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ReadWriteDirectories=/etc/ssl/caddy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">; The following additional security directives only work with systemd v229 or later.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">; They further restrict privileges that can be gained by caddy. Uncomment if you like.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">; Note that you may have to add capabilities required by any plugins in use.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">;CapabilityBoundingSet=CAP_NET_BIND_SERVICE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">;AmbientCapabilities=CAP_NET_BIND_SERVICE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">;NoNewPrivileges=true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[Install]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">WantedBy=multi-user.target
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="安装-hugo">安装 Hugo
&lt;/h3>&lt;p>本来想用 GitHub Actions 的，这样的话可以解决一部分国内的网络问题。但是后来发现步骤有点繁琐，不如简单的 WebHook，所以就只好在 Caddy 所在的服务器上也安装 Hugo。&lt;/p>
&lt;p>安装很简单，直接到 &lt;a class="link" href="https://github.com/gohugoio/hugo/releases" target="_blank" rel="noopener"
>https://github.com/gohugoio/hugo/releases&lt;/a> 下载一个 deb 包然后 &lt;code>sudo dpkg -i&lt;/code> 即可。&lt;/p>
&lt;p>官方推荐用 HomeBrew，但是 HomeBrew for Linux 的安装又需要用到 raw.githubusercontent.com，服务器直接装不上，于是作罢。&lt;/p>
&lt;h2 id="github-仓库">GitHub 仓库
&lt;/h2>&lt;h3 id="初始化">初始化
&lt;/h3>&lt;p>建一个仓库，例如 &lt;code>https://github.com/robinWongM/rwong-tech-blog&lt;/code>，然后 clone 到本地。&lt;/p>
&lt;p>根据 Hugo 的 &lt;a class="link" href="https://gohugo.io/getting-started/quick-start/" target="_blank" rel="noopener"
>QuickStart&lt;/a>，在仓库根目录建一个 config.toml。目前这边用的是：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">baseURL&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;https://rwong.tech/&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">languageCode&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;zh-CN&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">title&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;蛙声一片&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">theme&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;even&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">permalinks&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">posts&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;/:year/:month/:title/&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用 Git Submodule 添加主题文件。比如我 fork 了一个叫 &lt;code>even&lt;/code> 的主题后：&lt;/p>
&lt;pre tabindex="0">&lt;code>git submodule add git@github.com:robinWongM/hugo-theme-even.git themes/even
&lt;/code>&lt;/pre>&lt;p>然后 commit 即可。&lt;/p>
&lt;h3 id="新建-webhook">新建 WebHook
&lt;/h3>&lt;p>在仓库的 Settings 里配置一个 WebHook。URL 与 secret 都是自定义的，比如目前我的 WebHook URL 就是 &lt;code>https://rwong.tech/github/webhook&lt;/code>。&lt;/p>
&lt;h2 id="配置-caddy--git-webhook">配置 Caddy + Git WebHook
&lt;/h2>&lt;p>HTTPS 证书由 Caddy 自动到 Let&amp;rsquo;s Encrypt 申请。这边用的是 DNS Challenge，搭配阿里云云解析的 API 食用。&lt;/p>
&lt;p>使用 alidns 的时候，需要设置环境变量 &lt;code>ALICLOUD_ACCESS_KEY&lt;/code> 和 &lt;code>ALICLOUD_SECRET_KEY&lt;/code>。（来源：https://github.com/colachg/caddy-alidns）&lt;/p>
&lt;p>这边采用的方法是直接修改 &lt;code>caddy.service&lt;/code>，在 &lt;code>[Service]&lt;/code> 一节下增加：&lt;/p>
&lt;pre tabindex="0">&lt;code>Environment=ALICLOUD_ACCESS_KEY=*accesskey_id*
Environment=ALICLOUD_ALICLOUD_SECRET_KEY=*accesskey_secret*
&lt;/code>&lt;/pre>&lt;p>在阿里云 RAM 访问控制下新建用户并赋予“编程访问”权限后，可以获得这两个值。其中 AccessKey Secret 只出现一次，所以需要妥善保存。&lt;/p>
&lt;p>对于权限问题，目前给这个子用户赋予的权限是云解析的全部只读权限 + 云解析 &lt;code>rwong.tech&lt;/code> 域名的写权限。&lt;/p>
&lt;p>根据之前的配置，这边使用的 &lt;code>Caddyfile&lt;/code> 如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Caddyfile" data-lang="Caddyfile">&lt;span class="line">&lt;span class="cl">&lt;span class="n">(alitls)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">tls&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">dns&lt;/span> &lt;span class="s">alidns&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">rwong.tech&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">import&lt;/span> alitls
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">root&lt;/span> &lt;span class="nd">/var/www/rwong-tech-blog/public&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">git&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">repo&lt;/span> &lt;span class="s">https://github.com/robinWongM/rwong-tech-blog.git&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">path&lt;/span> &lt;span class="s">../&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">branch&lt;/span> &lt;span class="s">master&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">interval&lt;/span> &lt;span class="s">-1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">hook&lt;/span> &lt;span class="s">/github/webhook&lt;/span> &lt;span class="s">**secret**&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">then&lt;/span> &lt;span class="s">hugo&lt;/span> &lt;span class="s">--destination=/var/www/rwong-tech-blog/public&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gh">ddns.rwong.tech&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">import&lt;/span> alitls
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="后记">后记
&lt;/h2>&lt;p>之后挑个时间叛逃到 Treafik（&lt;/p></description></item></channel></rss>