<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>stackli</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://stackli.me/</id>
  <link href="https://stackli.me/" rel="alternate"/>
  <link href="https://stackli.me/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, stackli</rights>
  <subtitle>上下求索</subtitle>
  <title>栈里</title>
  <updated>2026-06-02T00:00:58.014Z</updated>
  <entry>
    <author>
      <name>stackli</name>
    </author>
    <category term="思与悟" scheme="https://stackli.me/categories/%E6%80%9D%E4%B8%8E%E6%82%9F/"/>
    <content>
      <![CDATA[<p>最近看了一段话，心有感悟，摘抄于此：</p><blockquote><p>柏拉图在《理想国》中探讨过一个问题：一个曾经走出洞穴、见过真正太阳的人，如果再被强行拉回黑暗的洞穴里，去和那些一辈子对着墙壁上的倒影狂欢的囚徒生活在一起，他所经历的痛苦，将是未曾见过光明的人所无法想象的。</p></blockquote><p>高中时代，在一位同学的介绍下，开始了解到了翻墙，打开了一个新世界的大门，原来这世上还存在另外一个很多人不知道的网络世界。学生时代没有钱，用的还是<strong>无界</strong>、<strong>自由门</strong>。虽然一打开软件就会跳转的一些特别的网站，但念在免费，也能够接受。那个时候没有自己的电脑，只能把软件保存在U盘中，等到去网吧或者学校机房时，就临时安装再使用。</p><p>上了大学之后，这些免费的翻墙软件就慢慢变得不好用了，又从一位同学那里了解到了GoAgent。 时间过去了太久，已经不记得GoAgent是怎么搭建了，只记得它是部署在google云上的，并且也可以免费使用。每次要翻墙之前，就通过终端打开GoAgent，然后在浏览器中配置代理就可以畅游了。至于翻墙工具的原理，当时我是完全不清楚的，都是照着一些教程一点一点配置。</p><p>再到后来，慢慢了解了翻墙的一些原理，但也仅限于知道它是通过代理实现的。GoAgent不好用之后，就知道了可以自己搭建代理。就在搬瓦工上买了一个VPS，使用它的面板一键搭建了ShadowSocks。比较穷，还是和朋友合用的，虽然要付费，但上网的速度确实觉得这钱花得值。好景不长，有一天这个vps服务器的ip再也无法访问了，可能是没有配置混淆，但当时不知道原因。</p><p>工作之后，又陆续购买了一些翻墙服务，前后断断续续的使用。 而且我记得是用了一个chrome插件， 每次遇到无法访问的页面，需要打开控制台，找到无法访问的域名，将域名配置到插件中。虽然可以使用，但用着还挺麻烦的。</p><p>后来，在一位同事的介绍下，了解到了机场，这下又打开了新世界的大门，原来翻墙还可以随心所欲。手机上、电脑上，乃至后面在路由器上都安装上了翻墙软件，自由地访问网络世界。对于机场主，我从内心是钦佩的，虽然赚了钱，但同时也冒着法律风险。我想他们就是我们这个时代的<strong>普罗米修斯</strong>吧，给我们带来了网络自由的火种。</p><p>但是在最近一段时间，机场也越来越不好用了。习惯了自由的网络，无法访问外网时，感觉被关进了笼子了。现在我的机场还在不断更新，也购买了备用机场，但内心也充满焦虑，不知道哪天那堵墙彻底封死，再也无法翻越。</p><hr><p>我本可以忍受黑暗，如果我不曾见过光明。 </p>]]>
    </content>
    <id>https://stackli.me/posts/2026/light-to-dark</id>
    <link href="https://stackli.me/posts/2026/light-to-dark"/>
    <published>2026-05-15T09:54:23.000Z</published>
    <summary>
      <![CDATA[<p>最近看了一段话，心有感悟，摘抄于此：</p>
<blockquote>
<p>柏拉图在《理想国》中探讨过一个问题：一个曾经走出洞穴、见过真正太阳的人，如果再被强行拉回黑暗的洞穴里，去和那些一辈子对着墙壁上的倒影狂欢的囚徒生活在一起，他所经历的痛苦，将是未曾见过光明的人所无法]]>
    </summary>
    <title>我本可以忍受黑暗，如果我不曾见过光明 -- 我的科学上网史</title>
    <updated>2026-06-02T00:00:58.014Z</updated>
  </entry>
  <entry>
    <author>
      <name>stackli</name>
    </author>
    <category term="AI" scheme="https://stackli.me/categories/AI/"/>
    <category term="AI" scheme="https://stackli.me/tags/AI/"/>
    <category term="opencode" scheme="https://stackli.me/tags/opencode/"/>
    <content>
      <![CDATA[<p>自从开始ai coding之后，工作时有了更多的空闲时间。正好最近也在用ai coding做一些个人项目，我就在想能不能在工作时间偶尔摸摸鱼，看看ai coding的运行情况。刚好看到opencode有web能力，可以通过浏览器访问，而家里有一台一直开机的nas。一拍即合，那就在nas上搭建一个opencode开发环境</p><span id="more"></span><h2 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h2><h3 id="Dockerfile"><a href="#Dockerfile" class="headerlink" title="Dockerfile"></a>Dockerfile</h3><p>opencode本身是直接支持通过docker安装的，但是开发时我们需要使用的git等工具，在官方的镜像中是没有包含的，因此我们需要自己创建一个Docker镜像，将需要用到的工具打包到镜像中去</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> ghcr.io/anomalyco/opencode:latest</span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> apk add --no-cache git openssh-client</span></span><br></pre></td></tr></table></figure><h3 id="docker-compose-yaml"><a href="#docker-compose-yaml" class="headerlink" title="docker-compose.yaml"></a>docker-compose.yaml</h3><p>创建一个docker-compose文件，端口根据需要进行修改。environment中的相关环境变量，放到<code>.env</code> 文件中，<code>.env</code>文件和<code>docker-compose.yaml</code>文件在同一个目录中。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">opencode:</span></span><br><span class="line">    <span class="attr">build:</span></span><br><span class="line">      <span class="attr">context:</span> <span class="string">.</span></span><br><span class="line">      <span class="attr">dockerfile:</span> <span class="string">Dockerfile.opencode</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">opencode</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;4096:4096&quot;</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">OPENCODE_SERVER_PASSWORD=$&#123;OPENCODE_SERVER_PASSWORD&#125;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">OPENCODE_SERVER_USERNAME=$&#123;OPENCODE_SERVER_USERNAME&#125;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">GIT_AUTHOR_NAME=$&#123;GIT_AUTHOR_NAME&#125;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">GIT_AUTHOR_EMAIL=$&#123;GIT_AUTHOR_EMAIL&#125;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">GIT_COMMITTER_NAME=$&#123;GIT_COMMITTER_NAME&#125;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">GIT_COMMITTER_EMAIL=$&#123;GIT_COMMITTER_EMAIL&#125;</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./.root:/root/</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./workspace:/workspace</span></span><br><span class="line">    <span class="attr">working_dir:</span> <span class="string">/workspace</span></span><br><span class="line">    <span class="attr">command:</span> [<span class="string">&quot;web&quot;</span>, <span class="string">&quot;--hostname&quot;</span>, <span class="string">&quot;0.0.0.0&quot;</span>, <span class="string">&quot;--port&quot;</span>, <span class="string">&quot;4096&quot;</span>]</span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br></pre></td></tr></table></figure><h3 id="env文件"><a href="#env文件" class="headerlink" title=".env文件"></a>.env文件</h3><p>环境变量配置模板如下</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">OPENCODE_SERVER_USERNAME=opencode</span><br><span class="line">OPENCODE_SERVER_PASSWORD=xxxx</span><br><span class="line">GIT_AUTHOR_NAME=&lt;username&gt;</span><br><span class="line">GIT_AUTHOR_EMAIL=&lt;email&gt;</span><br><span class="line">GIT_COMMITTER_NAME=&lt;username&gt;</span><br><span class="line">GIT_COMMITTER_EMAIL=&lt;email&gt;</span><br></pre></td></tr></table></figure><h2 id="OpenCode启动与验证"><a href="#OpenCode启动与验证" class="headerlink" title="OpenCode启动与验证"></a>OpenCode启动与验证</h2><p>在nas上完成上一步的准备工作后，启动docker, 通过 4096 端口就可以访问opencode的web服务。按照opencode官方文档进行大模型接入配置，就可以快乐开始vibe coding了。<br><img src="/static/image/2026/20260508_o3hd.png" alt="OpenCode界面"></p><h2 id="Git配置"><a href="#Git配置" class="headerlink" title="Git配置"></a>Git配置</h2><p>进行git配置前需要进入docker容器。</p><h3 id="SSH配置"><a href="#SSH配置" class="headerlink" title="SSH配置"></a>SSH配置</h3><ol><li>创建ssh key: <code>ssh-keygen -t ed25519 -C &quot;email@example.com&quot;</code> , 其中 passphrase 可以直接留空</li><li>将生成的公钥, 文件以<code>.pub</code>结尾，配置到github: Setting -&gt; SSH and GPG keys 页面中的SSH keys.  Key Type为<em>Authentication Key</em></li><li>尝试通过ssh来clone一下仓库，验证配置的正确性</li></ol><h3 id="commit签名配置"><a href="#commit签名配置" class="headerlink" title="commit签名配置"></a>commit签名配置</h3><p>github支持ssh签名和gpg签名，其中ssh签名配置比较简单，我也选择使用了ssh签名</p><ol><li>创建ssh key: <code>ssh-keygen -t ed25519 -C &quot;email@example.com&quot;</code> , 其中 passphrase 可以直接留空, 注意文件名修改下，不要和ssh登录的文件名相同，否则会覆盖之前生成的key。 这里我们命名为 <code>sign_ed25519</code></li><li>将生成的公钥, 文件以<code>.pub</code>结尾，配置到github: Setting -&gt; SSH and GPG keys 页面中的SSH keys.  Key Type为<em>Signing Key</em></li><li>开启commit加签配置<ol><li><code>git config --global user.signingkey ~/.ssh/sign_ed25519.pub</code></li><li><code>git config --global user.signingkey</code></li><li><code>git config --global commit.gpgsign true</code></li></ol></li><li>尝试做一个提交验证配置的正确性，在github上查看commit是否有签名</li></ol><hr><p>如此，再利用内网穿透，就可以在上班的时候打开浏览器，随时看下ai打工人的进度啦</p>]]>
    </content>
    <id>https://stackli.me/posts/2026/opencode-web-on-nas</id>
    <link href="https://stackli.me/posts/2026/opencode-web-on-nas"/>
    <published>2026-05-08T12:47:10.000Z</published>
    <summary>
      <![CDATA[<p>自从开始ai coding之后，工作时有了更多的空闲时间。正好最近也在用ai coding做一些个人项目，我就在想能不能在工作时间偶尔摸摸鱼，看看ai coding的运行情况。刚好看到opencode有web能力，可以通过浏览器访问，而家里有一台一直开机的nas。一拍即合，那就在nas上搭建一个opencode开发环境</p>]]>
    </summary>
    <title>上班摸鱼之在NAS上使用OpenCode搭建AI Coding环境</title>
    <updated>2026-06-02T00:00:58.014Z</updated>
  </entry>
  <entry>
    <author>
      <name>stackli</name>
    </author>
    <category term="系统与网络" scheme="https://stackli.me/categories/%E7%B3%BB%E7%BB%9F%E4%B8%8E%E7%BD%91%E7%BB%9C/"/>
    <category term="网络安全" scheme="https://stackli.me/categories/%E7%B3%BB%E7%BB%9F%E4%B8%8E%E7%BD%91%E7%BB%9C/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <category term="安全" scheme="https://stackli.me/tags/%E5%AE%89%E5%85%A8/"/>
    <content>
      <![CDATA[<p>购买VPS之后，为了便于日常访问，服务器的22端口通常是针对公网开放的。如果你只设置了密码登录，那你的VPS就处于被爆破的风险之中。使用密钥登录虽然能提升安全性，但是密钥保存在某一台设备中，我们更换设备会比较麻烦。因此本文就介绍了一种使用TOTP进行2FA的登录验证方式，即保证了安全性，也不丢失便捷性。</p><p>这种方式登录流程为：<br>先输入<strong>系统账号密码</strong> → 再输入<strong>手机 Google Authenticator 显示的 6 位验证码</strong> → 登录成功。</p><span id="more"></span><p>准备工作:</p><ul><li>一台VPS服务器</li><li>在手机上安装<strong>Google Authenticator</strong>软件</li></ul><hr><div class="tag-plugin colorful note" color="warning"><div class="title">重要提醒:</div><div class="body"><p>配置过程中<strong>不要关闭当前的 SSH 窗口</strong>，以防配置错误把自己锁在外面。建议先在测试服务器上验证。</p></div></div><h3 id="Authenticator-PAM-模块安装"><a href="#Authenticator-PAM-模块安装" class="headerlink" title="Authenticator PAM 模块安装"></a>Authenticator PAM 模块安装</h3><p><strong>Ubuntu &#x2F; Debian：</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt install libpam-google-authenticator -y</span><br></pre></td></tr></table></figure><p><strong>CentOS &#x2F; RHEL &#x2F; Rocky &#x2F; AlmaLinux &#x2F; Fedora：</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> dnf install epel-release -y</span><br><span class="line"><span class="built_in">sudo</span> dnf install google-authenticator -y</span><br></pre></td></tr></table></figure><h3 id="为用户生成-TOTP-密钥"><a href="#为用户生成-TOTP-密钥" class="headerlink" title="为用户生成 TOTP 密钥"></a>为用户生成 TOTP 密钥</h3><div class="tag-plugin colorful note" color="warning"><div class="body"><p>必须以要登录的用户身份执行</p></div></div><p>切换到需要启用 2FA 的普通用户（强烈建议不要对 root 用户做此操作）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">su - yourusername</span><br><span class="line">google-authenticator</span><br></pre></td></tr></table></figure><p>按以下流程进行：</p><ol><li>Do you want authentication tokens to be time-based (y&#x2F;n) → 输入: <strong>y</strong></li><li>屏幕上会出现一个二维码，用手机 <strong>Google Authenticator</strong> App 扫描添加账户</li><li>输入<strong>Google Authenticator</strong>中的验证码</li><li>会生成<strong>5 个紧急恢复码（scratch codes）</strong> —— <strong>立即复制保存到安全地方</strong>（手机丢失时使用）,也会保存在用户家目录：<code>~/.google_authenticator</code></li></ol><p>后续问答推荐按下面进行</p><ul><li>Update the .google_authenticator file? → <strong>y</strong></li><li>Disallow multiple uses of the same authentication token? → <strong>y</strong></li><li>Increase the time skew window? → <strong>n</strong>（或根据需要）</li><li>Do you want to enable rate-limiting? → <strong>y</strong></li></ul><h3 id="配置-PAM-模块"><a href="#配置-PAM-模块" class="headerlink" title="配置 PAM 模块"></a>配置 PAM 模块</h3><p>编辑 SSH 的 PAM 配置文件：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> vim /etc/pam.d/sshd</span><br></pre></td></tr></table></figure><p>在文件<strong>最顶部</strong>添加以下一行（推荐放在最前面）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">auth required pam_unix.so nullok try_first_pass</span><br><span class="line">auth required pam_google_authenticator.so nullok</span><br></pre></td></tr></table></figure><ul><li><code>nullok</code> 表示：如果用户还没有生成 <code>.google_authenticator</code> 文件，则只用密码也能登录（初期设置时非常有用）。全部用户配置完成后，<strong>建议删除 <code>nullok</code></strong> 以强制要求 2FA。</li><li>注意以上两行的顺序，和登录时输入密码和TOTP的顺序有关</li></ul><p>保存退出。</p><h3 id="配置-SSH-服务（sshd-config）"><a href="#配置-SSH-服务（sshd-config）" class="headerlink" title="配置 SSH 服务（sshd_config）"></a>配置 SSH 服务（sshd_config）</h3><p>编辑配置文件（推荐新建独立配置文件，避免覆盖系统默认）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> vim /etc/ssh/sshd_config.d/2fa.conf</span><br></pre></td></tr></table></figure><p>（如果你的系统没有 <code>sshd_config.d/</code> 目录，可直接编辑 <code>/etc/ssh/sshd_config</code>）</p><p>添加以下内容：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 启用键盘交互认证（用于输入密码和 TOTP）</span></span><br><span class="line">KbdInteractiveAuthentication <span class="built_in">yes</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 启用 PAM</span></span><br><span class="line">UsePAM <span class="built_in">yes</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 强制使用 keyboard-interactive（这样会先走密码，再走 TOTP）</span></span><br><span class="line">AuthenticationMethods keyboard-interactive</span><br><span class="line"></span><br><span class="line"><span class="comment"># 禁用直接密码认证（防止绕过 keyboard-interactive）</span></span><br><span class="line">PasswordAuthentication no</span><br><span class="line"></span><br><span class="line"><span class="comment"># 推荐禁用 root 直接登录</span></span><br><span class="line">PermitRootLogin no</span><br><span class="line"></span><br><span class="line"><span class="comment"># 建议开启公钥认证（即使不强制使用，也可留作备用）</span></span><br><span class="line">PubkeyAuthentication <span class="built_in">yes</span></span><br></pre></td></tr></table></figure><p><strong>说明</strong>：</p><ul><li><code>KbdInteractiveAuthentication yes</code>（新版 OpenSSH 推荐，旧版可使用 <code>ChallengeResponseAuthentication yes</code>）</li><li><code>AuthenticationMethods keyboard-interactive</code> 是关键，确保整个认证过程走 PAM，从而依次提示密码 + TOTP。</li></ul><p>测试配置语法是否正确：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> sshd -t</span><br></pre></td></tr></table></figure><h3 id="重启-SSH-服务"><a href="#重启-SSH-服务" class="headerlink" title="重启 SSH 服务"></a>重启 SSH 服务</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl restart sshd</span><br></pre></td></tr></table></figure><h3 id="测试登录"><a href="#测试登录" class="headerlink" title="测试登录"></a>测试登录</h3><p>从另一台电脑&#x2F;终端测试：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh yourusername@your-server-ip</span><br></pre></td></tr></table></figure><p>预期登录流程：</p><ol><li>输入 <strong>系统登录密码</strong></li><li>提示 <code>Verification code:</code>（或类似），输入手机 App 当前显示的 <strong>6 位 TOTP 验证码</strong></li><li>登录成功</li></ol><h3 id="常见问题处理"><a href="#常见问题处理" class="headerlink" title="常见问题处理"></a>常见问题处理</h3><ul><li><p><strong>时间不同步</strong>：服务器时间必须与手机同步。建议安装 chrony 或 ntp：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install chrony -y   <span class="comment"># Ubuntu</span></span><br><span class="line"><span class="comment"># 或</span></span><br><span class="line"><span class="built_in">sudo</span> dnf install chrony -y   <span class="comment"># RHEL系</span></span><br><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> --now chronyd</span><br></pre></td></tr></table></figure></li><li><p><strong>登录失败（Permission denied）</strong>：检查 <code>sshd -t</code> 是否报错，查看 <code>/var/log/auth.log</code> 或 <code>/var/log/secure</code> 日志。</p></li><li><p><strong>想临时允许未设置 2FA 的用户</strong>：保留 <code>nullok</code>。</p></li><li><p><strong>强制所有用户必须用 2FA</strong>：把 PAM 行改为 <code>auth required pam_google_authenticator.so</code>（去掉 nullok）。</p></li><li><p><strong>同时支持公钥 + 密码 + 2FA</strong>（更灵活，但安全性稍低）：把 <code>AuthenticationMethods</code> 改成：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">AuthenticationMethods publickey,password publickey,keyboard-interactive keyboard-interactive</span><br></pre></td></tr></table></figure><p>并把 <code>PasswordAuthentication yes</code>。</p></li></ul><p>配置完成后，建议：</p><ul><li>保存好紧急恢复码</li><li>测试多次成功后，再从其他地方尝试登录</li><li>定期备份 <code>~/.google_authenticator</code> 文件</li></ul>]]>
    </content>
    <id>https://stackli.me/posts/2026/ssh-2fa-config</id>
    <link href="https://stackli.me/posts/2026/ssh-2fa-config"/>
    <published>2026-04-10T11:40:22.000Z</published>
    <summary>
      <![CDATA[<p>购买VPS之后，为了便于日常访问，服务器的22端口通常是针对公网开放的。如果你只设置了密码登录，那你的VPS就处于被爆破的风险之中。使用密钥登录虽然能提升安全性，但是密钥保存在某一台设备中，我们更换设备会比较麻烦。因此本文就介绍了一种使用TOTP进行2FA的登录验证方式，即保证了安全性，也不丢失便捷性。</p>
<p>这种方式登录流程为：<br>先输入<strong>系统账号密码</strong> → 再输入<strong>手机 Google Authenticator 显示的 6 位验证码</strong> → 登录成功。</p>]]>
    </summary>
    <title>给SSH配置2FA登录认证，和黑客说拜拜</title>
    <updated>2026-06-02T00:00:58.014Z</updated>
  </entry>
  <entry>
    <author>
      <name>stackli</name>
    </author>
    <category term="系统与网络" scheme="https://stackli.me/categories/%E7%B3%BB%E7%BB%9F%E4%B8%8E%E7%BD%91%E7%BB%9C/"/>
    <category term="网络工具" scheme="https://stackli.me/categories/%E7%B3%BB%E7%BB%9F%E4%B8%8E%E7%BD%91%E7%BB%9C/%E7%BD%91%E7%BB%9C%E5%B7%A5%E5%85%B7/"/>
    <category term="headscale" scheme="https://stackli.me/tags/headscale/"/>
    <category term="tailscale" scheme="https://stackli.me/tags/tailscale/"/>
    <content>
      <![CDATA[<p>在之前的文章<a href="/2026-03-19-tailscale-subnet">Tailscale子网路由部署</a>中介绍了如何使用子路由来访问没有安装tailscale的设备，这个功能用到了系统的流量转发能力，即把流量转发给子网的机器。tailscale除了子网路由功能，还有一种功能是出口节点功能，即把流量转发给外部。</p><span id="more"></span><p>举一个我们经常会用到的场景。在这么一个网络环境中，家里的设备希望访问<code>google.com</code>, 由于某些原因，网络无法直接访问。但是我们有一台处于境外的VPS，可以直接访问到google.com。这时，我们可以将这个VPS加入tailscale节点，并设置为<code>Exit Node</code>, 那么网络中的其他设备就可以将这个VPS节点作为Exit Node，所有的流量都会发送到这个Exit Node, 再由Exit Node转发到目标服务器。<br><img src="/static/image/2026/20260325_tailnet_exit_node.png" alt="20260325_tailnet_exit_node"></p><p>除了这个场景，Exit Node还能起到隐私保护，流量安全等不敌</p><h2 id="部署步骤"><a href="#部署步骤" class="headerlink" title="部署步骤"></a>部署步骤</h2><p>关于如何安装tailscale，请参考<a href="/2026-03-19-headscale-deploy">Headscale搭建过程实录</a>。</p><h3 id="开启转发功能"><a href="#开启转发功能" class="headerlink" title="开启转发功能"></a>开启转发功能</h3><p>开启转发功能，可以参考: <a href="/2026-03-19-tailscale-subnet">Tailscale子网路由部署</a></p><h3 id="重启tailscale服务开启Exit-Node"><a href="#重启tailscale服务开启Exit-Node" class="headerlink" title="重启tailscale服务开启Exit Node"></a>重启tailscale服务开启Exit Node</h3><p>如果你是自己部署的headscale服务，使用命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 192.168.0.0/24 需要根据实际情况修改</span></span><br><span class="line">tailscale up --advertise-exit-node --login-server=https://hs.example.com</span><br></pre></td></tr></table></figure><p>如果是使用tailscale官方服务，使用命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 192.168.0.0/24 需要根据实际情况修改</span></span><br><span class="line">tailscale up --advertise-exit-node</span><br></pre></td></tr></table></figure><h3 id="在网页批准子网路由"><a href="#在网页批准子网路由" class="headerlink" title="在网页批准子网路由"></a>在网页批准子网路由</h3><p>连接成功后，进入对应节点的配置页面，点击“编辑”：<br><img src="/static/image/2026/20260325_tailscale_exit_approve.png" alt="20260325_tailscale_exit_approve"><br>然后Approve并保存,就成功开启了Exit Node～～</p><h3 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h3><p>在任意一台开启tailsale的设备中，选择对应的<code>Exit Node</code>并启动连接，尝试进行访问<br><img src="/static/image/2026/20260325_tailscale_exit_client.png" alt="20260325_tailscale_exit_client"></p>]]>
    </content>
    <id>https://stackli.me/posts/2026/tailscale-exit-node</id>
    <link href="https://stackli.me/posts/2026/tailscale-exit-node"/>
    <published>2026-03-25T11:56:10.000Z</published>
    <summary>
      <![CDATA[<p>在之前的文章<a href="/2026-03-19-tailscale-subnet">Tailscale子网路由部署</a>中介绍了如何使用子路由来访问没有安装tailscale的设备，这个功能用到了系统的流量转发能力，即把流量转发给子网的机器。tailscale除了子网路由功能，还有一种功能是出口节点功能，即把流量转发给外部。</p>]]>
    </summary>
    <title>Tailscale出口节点(Exit Node)配置</title>
    <updated>2026-06-02T00:00:58.014Z</updated>
  </entry>
  <entry>
    <author>
      <name>stackli</name>
    </author>
    <category term="系统与网络" scheme="https://stackli.me/categories/%E7%B3%BB%E7%BB%9F%E4%B8%8E%E7%BD%91%E7%BB%9C/"/>
    <category term="网络工具" scheme="https://stackli.me/categories/%E7%B3%BB%E7%BB%9F%E4%B8%8E%E7%BD%91%E7%BB%9C/%E7%BD%91%E7%BB%9C%E5%B7%A5%E5%85%B7/"/>
    <category term="headscale" scheme="https://stackli.me/tags/headscale/"/>
    <category term="tailscale" scheme="https://stackli.me/tags/tailscale/"/>
    <content>
      <![CDATA[<p>在之前的文章<a href="/2026-03-19-headscale-deploy">Headscale搭建过程实录</a>中我们介绍了如何搭建一个Headscale服务，并成功将Tailscale加入到Headscale中。安装了Tailscale并成功接入的设备，可以像局域网一样互相访问。<br>但是在我们的家中，不是每一台设备都是可以安装Tailscale的，这时就需要用到Tailscale的子网路由功能了。</p><span id="more"></span><p>这是一张来自官网的示意图，可以帮助我们快速了解什么是子网路由：<br><img src="/static/image/2026/20260322_tailscale_subnet_router.png" alt="20260322_tailscale_subnet_router"><br>在图中，左边是加入了tailscale网络的节点，右边是另外的一个独立子网。在子网中，我们挑选一台设备安装tailscale，并开启子网路由功能。那么已经加入tailscale的其他设备（图左），就可以直接使用子网的机器ip，例如<code>192.168.0.2</code>，进行直接访问。</p><h2 id="部署步骤"><a href="#部署步骤" class="headerlink" title="部署步骤"></a>部署步骤</h2><p>关于如何安装tailscale，请参考<a href="/2026-03-19-headscale-deploy">Headscale搭建过程实录</a>。</p><h3 id="开启转发功能"><a href="#开启转发功能" class="headerlink" title="开启转发功能"></a>开启转发功能</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;net.ipv4.ip_forward = 1&#x27;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> -a /etc/sysctl.d/99-tailscale.conf</span><br><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;net.ipv6.conf.all.forwarding = 1&#x27;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> -a /etc/sysctl.d/99-tailscale.conf</span><br><span class="line"><span class="built_in">sudo</span> sysctl -p /etc/sysctl.d/99-tailscale.conf</span><br></pre></td></tr></table></figure><p>如果没有<code>/etc/sysctl.d</code>目录，则执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;net.ipv4.ip_forward = 1&#x27;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> -a /etc/sysctl.conf</span><br><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;net.ipv6.conf.all.forwarding = 1&#x27;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> -a /etc/sysctl.conf</span><br><span class="line"><span class="built_in">sudo</span> sysctl -p /etc/sysctl.conf</span><br></pre></td></tr></table></figure><div class="tag-plugin colorful note" color="cyan"><div class="title">我用的是alpine系统，重启之后，forward可能会失效。</div><div class="body"><p>需要使用命令<code>rc-update add sysctl boot</code>启用sysctl服务</p></div></div><h3 id="重启tailscale服务并广播子网"><a href="#重启tailscale服务并广播子网" class="headerlink" title="重启tailscale服务并广播子网"></a>重启tailscale服务并广播子网</h3><p>如果你是自己部署的headscale服务，使用命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 192.168.0.0/24 需要根据实际情况修改</span></span><br><span class="line">tailscale up --accept-routes --advertise-routes=192.168.0.0/24 --login-server=https://hs.example.com</span><br></pre></td></tr></table></figure><p>如果是使用tailscale官方服务，使用命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 192.168.0.0/24 需要根据实际情况修改</span></span><br><span class="line">tailscale up --advertise-routes=192.168.1.0/24 --accept-routes</span><br></pre></td></tr></table></figure><h3 id="在网页批准子网路由"><a href="#在网页批准子网路由" class="headerlink" title="在网页批准子网路由"></a>在网页批准子网路由</h3><p>连接成功后，进入对应节点的配置页面，点击“编辑”：<br><img src="/static/image/2026/20260322_plane_machine.png" alt="20260322_plane_machine"><br>然后Approve并保存:<br><img src="/static/image/2026/20260322_plane_subnet_route_approve.png" alt="20260322_plane_subnet_route_approve"><br>就成功开启了子网路由～～</p><h3 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h3><p>在任意一台开启tailsale的设备中，直接ping子网中的机器ip，例如<code>192.168.0.2</code>。如果能正常受到响应，则说明子网路由成功。</p>]]>
    </content>
    <id>https://stackli.me/posts/2026/tailscale-subnet</id>
    <link href="https://stackli.me/posts/2026/tailscale-subnet"/>
    <published>2026-03-22T09:31:45.000Z</published>
    <summary>
      <![CDATA[<p>在之前的文章<a href="/2026-03-19-headscale-deploy">Headscale搭建过程实录</a>中我们介绍了如何搭建一个Headscale服务，并成功将Tailscale加入到Headscale中。安装了Tailscale并成功接入的设备，可以像局域网一样互相访问。<br>但是在我们的家中，不是每一台设备都是可以安装Tailscale的，这时就需要用到Tailscale的子网路由功能了。</p>]]>
    </summary>
    <title>Tailscale子网路由（Subnet routers）部署</title>
    <updated>2026-06-02T00:00:58.014Z</updated>
  </entry>
  <entry>
    <author>
      <name>stackli</name>
    </author>
    <category term="系统与网络" scheme="https://stackli.me/categories/%E7%B3%BB%E7%BB%9F%E4%B8%8E%E7%BD%91%E7%BB%9C/"/>
    <category term="网络工具" scheme="https://stackli.me/categories/%E7%B3%BB%E7%BB%9F%E4%B8%8E%E7%BD%91%E7%BB%9C/%E7%BD%91%E7%BB%9C%E5%B7%A5%E5%85%B7/"/>
    <category term="headscale" scheme="https://stackli.me/tags/headscale/"/>
    <category term="tailscale" scheme="https://stackli.me/tags/tailscale/"/>
    <content>
      <![CDATA[<p>如果你有一个NAS，或者有一个家庭服务器，你一定想过如果能够随时随地访问到家里的网络。也许你有一个云服务器，不希望暴露ssh端口到公网。<br>tailscale&#x2F;headscale就能实现我们这个需求，接入的设备可以像内网一样，轻松实现互相访问。其中tailscale需要依赖tailscale官方提供的元数据服务，负责协调和管理网络。而headscale则是其开源实现，我们使用headscale来实现完全自主的私有内容。<br>本文就介绍了使用headscale搭建一个完全自主的私有内网，并使用Caddy作为反向代理，实现域名解析和端口转发。</p><span id="more"></span><h2 id="部署架构"><a href="#部署架构" class="headerlink" title="部署架构"></a>部署架构</h2><h3 id="目录结构"><a href="#目录结构" class="headerlink" title="目录结构"></a>目录结构</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">/opt/</span><br><span class="line">├── caddy/</span><br><span class="line">│   ├── docker-compose.yml</span><br><span class="line">│   ├── Caddyfile</span><br><span class="line">│   ├── data/                  # 证书等持久数据（自动生成）</span><br><span class="line">│   └── config/                # Caddy 运行时配置（自动生成）</span><br><span class="line">│</span><br><span class="line">└── headscale/</span><br><span class="line">    ├── docker-compose.yml</span><br><span class="line">    ├── headscale/</span><br><span class="line">    │   ├── config/</span><br><span class="line">    │   │   └── config.yaml</span><br><span class="line">    │   └── data/              # db、私钥等持久数据</span><br><span class="line">    └── headplane/</span><br><span class="line">        └── config/</span><br><span class="line">            └── config.yaml</span><br></pre></td></tr></table></figure><h3 id="网络架构"><a href="#网络架构" class="headerlink" title="网络架构"></a>网络架构</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">                        Internet</span><br><span class="line">                            │</span><br><span class="line">               ┌────────────┼────────────┐</span><br><span class="line">           TCP 80/443    UDP 3478    UDP 41641</span><br><span class="line">               │             │            │</span><br><span class="line">               ▼             │            │</span><br><span class="line">            Caddy            │            │</span><br><span class="line">               │             │            │</span><br><span class="line">       ┌───────┴───────┐     │            │</span><br><span class="line">       │               │     │            │</span><br><span class="line">  hs.example.com  hp.example.com          │</span><br><span class="line">       │               │                  │</span><br><span class="line">       ▼               ▼                  │</span><br><span class="line">  headscale:8080  headplane:3000          │</span><br><span class="line">       │                                  │</span><br><span class="line">       └──────── headscale:3478 ◄─────────┘</span><br><span class="line">                 headscale:41641◄─────────┘</span><br><span class="line"></span><br><span class="line">Docker 网络:</span><br><span class="line">  caddy-net     : caddy ↔ headscale ↔ headplane</span><br><span class="line">  headscale-net : headscale ↔ headplane 内部通信</span><br></pre></td></tr></table></figure><h3 id="域名"><a href="#域名" class="headerlink" title="域名"></a>域名</h3><table><thead><tr><th>域名</th><th>用途</th></tr></thead><tbody><tr><td><code>hs.example.com</code></td><td>Headscale 主服务 + 内置 DERP</td></tr><tr><td><code>hp.example.com</code></td><td>Headplane 管理面板</td></tr></tbody></table><h3 id="服务器防火墙端口"><a href="#服务器防火墙端口" class="headerlink" title="服务器防火墙端口"></a>服务器防火墙端口</h3><table><thead><tr><th>端口</th><th>协议</th><th>用途</th><th>对外开放</th></tr></thead><tbody><tr><td>80</td><td>TCP</td><td>HTTP &#x2F; ACME 证书验证</td><td>✅</td></tr><tr><td>443</td><td>TCP</td><td>HTTPS 入口</td><td>✅</td></tr><tr><td>443</td><td>UDP</td><td>HTTP&#x2F;3</td><td>✅</td></tr><tr><td>3478</td><td>UDP</td><td>DERP STUN</td><td>✅</td></tr><tr><td>41641</td><td>UDP</td><td>DERP 直连备用</td><td>✅</td></tr></tbody></table><hr><h2 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h2><h3 id="初始化目录"><a href="#初始化目录" class="headerlink" title="初始化目录"></a>初始化目录</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> -p /opt/caddy/&#123;data,config&#125;</span><br><span class="line"><span class="built_in">mkdir</span> -p /opt/headscale/headscale/&#123;config,data&#125;</span><br><span class="line"><span class="built_in">mkdir</span> -p /opt/headscale/headplane/config</span><br></pre></td></tr></table></figure><h3 id="初始化网络"><a href="#初始化网络" class="headerlink" title="初始化网络"></a>初始化网络</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker network create caddy-net</span><br></pre></td></tr></table></figure><hr><h2 id="配置文件"><a href="#配置文件" class="headerlink" title="配置文件"></a>配置文件</h2><h3 id="headscale配置文件"><a href="#headscale配置文件" class="headerlink" title="headscale配置文件"></a>headscale配置文件</h3><div class="tag-plugin tabs" align="center"id="tab_1"><div class="nav-tabs"><div class="tab active"><a href="#tab_1-1">docker-compose</a></div><div class="tab"><a href="#tab_1-2">headscale配置</a></div><div class="tab"><a href="#tab_1-3">headplane配置</a></div></div><div class="tab-content"><div class="tab-pane active" id="tab_1-1"><p>路径: <code>/opt/headscale/docker-compose.yml</code></p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">networks:</span></span><br><span class="line">  <span class="attr">headscale-net:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">headscale-net</span></span><br><span class="line">    <span class="attr">driver:</span> <span class="string">bridge</span></span><br><span class="line">  <span class="attr">caddy-net:</span></span><br><span class="line">    <span class="attr">external:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">headscale:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">ghcr.io/juanfont/headscale:latest</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">headscale</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line">    <span class="attr">command:</span> <span class="string">serve</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./headscale/config:/etc/headscale:ro</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./headscale/data:/var/lib/headscale</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;3478:3478/udp&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;41641:41641/udp&quot;</span></span><br><span class="line">    <span class="attr">cap_add:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">NET_ADMIN</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYS_MODULE</span></span><br><span class="line">    <span class="attr">sysctls:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">net.ipv4.ip_forward=1</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">net.ipv6.conf.all.forwarding=1</span></span><br><span class="line">    <span class="attr">networks:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">headscale-net</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">caddy-net</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;headscale&quot;</span>, <span class="string">&quot;health&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">30s</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">10s</span></span><br><span class="line">      <span class="attr">retries:</span> <span class="number">3</span></span><br><span class="line">      <span class="attr">start_period:</span> <span class="string">10s</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">headplane:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">ghcr.io/tale/headplane:latest</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">headplane</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="attr">headscale:</span></span><br><span class="line">        <span class="attr">condition:</span> <span class="string">service_healthy</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./headplane/config:/etc/headplane:ro</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./headscale/config/config.yaml:/etc/headscale/config.yaml:ro</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">/var/run/docker.sock:/var/run/docker.sock:ro</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">HEADSCALE_URL=http://headscale:8080</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">HEADSCALE_INTEGRATION=docker</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">HEADSCALE_CONTAINER=headscale</span></span><br><span class="line">    <span class="attr">networks:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">headscale-net</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">caddy-net</span></span><br><span class="line"></span><br></pre></td></tr></table></figure></div><div class="tab-pane" id="tab_1-2"><p>路径: <code>/opt/headscale/headscale/config/config.yaml</code></p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">server_url:</span> <span class="string">https://hs.example.com</span></span><br><span class="line"></span><br><span class="line"><span class="attr">listen_addr:</span> <span class="number">0.0</span><span class="number">.0</span><span class="number">.0</span><span class="string">:8080</span></span><br><span class="line"><span class="attr">metrics_listen_addr:</span> <span class="number">0.0</span><span class="number">.0</span><span class="number">.0</span><span class="string">:9090</span></span><br><span class="line"><span class="attr">grpc_listen_addr:</span> <span class="number">0.0</span><span class="number">.0</span><span class="number">.0</span><span class="string">:50443</span></span><br><span class="line"><span class="attr">grpc_allow_insecure:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="attr">tls_cert_path:</span> <span class="string">&quot;&quot;</span></span><br><span class="line"><span class="attr">tls_key_path:</span> <span class="string">&quot;&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">noise:</span></span><br><span class="line">  <span class="attr">private_key_path:</span> <span class="string">/var/lib/headscale/noise_private_key</span></span><br><span class="line"></span><br><span class="line"><span class="attr">prefixes:</span></span><br><span class="line">  <span class="attr">v4:</span> <span class="number">100.64</span><span class="number">.0</span><span class="number">.0</span><span class="string">/10</span></span><br><span class="line">  <span class="attr">v6:</span> <span class="string">fd7a:115c:a1e0::/48</span></span><br><span class="line">  <span class="attr">allocation:</span> <span class="string">sequential</span></span><br><span class="line"></span><br><span class="line"><span class="attr">derp:</span></span><br><span class="line">  <span class="attr">server:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">region_id:</span> <span class="number">999</span></span><br><span class="line">    <span class="attr">region_code:</span> <span class="string">&quot;custom&quot;</span></span><br><span class="line">    <span class="attr">region_name:</span> <span class="string">&quot;Custom DERP&quot;</span></span><br><span class="line">    <span class="attr">stun_listen_addr:</span> <span class="string">&quot;0.0.0.0:3478&quot;</span></span><br><span class="line">    <span class="attr">private_key_path:</span> <span class="string">/var/lib/headscale/derp_server_private_key</span></span><br><span class="line">  <span class="attr">urls:</span> []</span><br><span class="line">  <span class="attr">paths:</span> []</span><br><span class="line">  <span class="attr">auto_update_enabled:</span> <span class="literal">false</span></span><br><span class="line">  <span class="attr">update_frequency:</span> <span class="string">24h</span></span><br><span class="line"></span><br><span class="line"><span class="attr">database:</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">sqlite</span></span><br><span class="line">  <span class="attr">sqlite:</span></span><br><span class="line">    <span class="attr">path:</span> <span class="string">/var/lib/headscale/db.sqlite</span></span><br><span class="line"></span><br><span class="line"><span class="attr">dns:</span></span><br><span class="line">  <span class="attr">magic_dns:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">base_domain:</span> <span class="string">ts.example.com</span></span><br><span class="line">  <span class="attr">nameservers:</span></span><br><span class="line">    <span class="attr">global:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="number">1.1</span><span class="number">.1</span><span class="number">.1</span></span><br><span class="line">      <span class="bullet">-</span> <span class="number">8.8</span><span class="number">.8</span><span class="number">.8</span></span><br><span class="line"></span><br><span class="line"><span class="attr">policy:</span></span><br><span class="line">  <span class="attr">mode:</span> <span class="string">database</span></span><br><span class="line"></span><br><span class="line"><span class="attr">log:</span></span><br><span class="line">  <span class="attr">level:</span> <span class="string">info</span></span><br><span class="line">  <span class="attr">format:</span> <span class="string">text</span></span><br><span class="line"></span><br><span class="line"><span class="attr">disable_check_updates:</span> <span class="literal">true</span></span><br><span class="line"><span class="attr">ephemeral_node_inactivity_timeout:</span> <span class="string">30m</span></span><br><span class="line"><span class="attr">node_update_check_interval:</span> <span class="string">10s</span></span><br><span class="line"></span><br></pre></td></tr></table></figure></div><div class="tab-pane" id="tab_1-3"><p>路径: <code>/opt/headscale/headplane/config/config.yaml</code></p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">server:</span></span><br><span class="line">  <span class="attr">host:</span> <span class="number">0.0</span><span class="number">.0</span><span class="number">.0</span></span><br><span class="line">  <span class="attr">port:</span> <span class="number">3000</span></span><br><span class="line">  <span class="comment"># 随机生成一个 64 位字符串</span></span><br><span class="line">  <span class="attr">cookie_secret:</span> <span class="string">&quot;your-random-secret-string-at-least-32-chars!!&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">headscale:</span></span><br><span class="line">  <span class="attr">url:</span> <span class="string">https://hs.example.com</span></span><br><span class="line">  <span class="attr">grpc_url:</span> <span class="string">http://headscale:50443</span></span><br><span class="line">  <span class="attr">config_strict:</span> <span class="literal">false</span></span><br><span class="line"></span><br></pre></td></tr></table></figure></div></div></div><h3 id="Caddy配置"><a href="#Caddy配置" class="headerlink" title="Caddy配置"></a>Caddy配置</h3><div class="tag-plugin tabs" align="center"id="tab_2"><div class="nav-tabs"><div class="tab active"><a href="#tab_2-1">docker-compose</a></div><div class="tab"><a href="#tab_2-2">Caddyfile</a></div></div><div class="tab-content"><div class="tab-pane active" id="tab_2-1"><p>路径: <code>/opt/caddy/docker-compose.yml</code></p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">networks:</span></span><br><span class="line">  <span class="attr">caddy-net:</span></span><br><span class="line">    <span class="attr">external:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">caddy:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">caddy:latest</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">caddy</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;80:80&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;443:443&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;443:443/udp&quot;</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./Caddyfile:/etc/caddy/Caddyfile:ro</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./data:/data</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./config:/config</span></span><br><span class="line">    <span class="attr">networks:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">caddy-net</span></span><br></pre></td></tr></table></figure></div><div class="tab-pane" id="tab_2-2"><p>路径: <code>/opt/caddy/Caddyfile</code></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    email mail@example.com</span><br><span class="line">    log &#123;</span><br><span class="line">        level INFO</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">hs.example.com &#123;</span><br><span class="line">    log &#123;</span><br><span class="line">        output file /data/logs/headscale.log &#123;</span><br><span class="line">            roll_size 50mb</span><br><span class="line">            roll_keep 5</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    reverse_proxy headscale:8080 &#123;</span><br><span class="line">        header_up Host &#123;host&#125;</span><br><span class="line">        header_up X-Real-IP &#123;remote_host&#125;</span><br><span class="line">        header_up X-Forwarded-For &#123;remote_host&#125;</span><br><span class="line">        header_up X-Forwarded-Proto &#123;scheme&#125;</span><br><span class="line">        transport http &#123;</span><br><span class="line">            keepalive 30s</span><br><span class="line">            keepalive_idle_conns 10</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">hp.example.com &#123;</span><br><span class="line">    log &#123;</span><br><span class="line">        output file /data/logs/headplane.log &#123;</span><br><span class="line">            roll_size 20mb</span><br><span class="line">            roll_keep 5</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    reverse_proxy headplane:3000 &#123;</span><br><span class="line">        header_up Host &#123;host&#125;</span><br><span class="line">        header_up X-Real-IP &#123;remote_host&#125;</span><br><span class="line">        header_up X-Forwarded-For &#123;remote_host&#125;</span><br><span class="line">        header_up X-Forwarded-Proto &#123;scheme&#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></div></div><hr><h2 id="部署步骤"><a href="#部署步骤" class="headerlink" title="部署步骤"></a>部署步骤</h2><h3 id="Step-1：配置域名解析"><a href="#Step-1：配置域名解析" class="headerlink" title="Step 1：配置域名解析"></a>Step 1：配置域名解析</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">hs.example.com  →  A  →  &lt;服务器公网IP&gt;</span><br><span class="line">hp.example.com  →  A  →  &lt;服务器公网IP&gt;</span><br></pre></td></tr></table></figure><h3 id="Step-2：开放防火墙端口"><a href="#Step-2：开放防火墙端口" class="headerlink" title="Step 2：开放防火墙端口"></a>Step 2：开放防火墙端口</h3><p>如果你的服务器部署在云上，则需要到对应的功能页面开启，无需执行下列命令</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Ubuntu/Debian (ufw)</span></span><br><span class="line">ufw allow 80/tcp</span><br><span class="line">ufw allow 443/tcp</span><br><span class="line">ufw allow 443/udp</span><br><span class="line">ufw allow 3478/udp</span><br><span class="line">ufw allow 41641/udp</span><br><span class="line">ufw reload</span><br><span class="line"></span><br><span class="line"><span class="comment"># CentOS/Rocky (firewalld)</span></span><br><span class="line">firewall-cmd --permanent --add-port=80/tcp</span><br><span class="line">firewall-cmd --permanent --add-port=443/tcp</span><br><span class="line">firewall-cmd --permanent --add-port=443/udp</span><br><span class="line">firewall-cmd --permanent --add-port=3478/udp</span><br><span class="line">firewall-cmd --permanent --add-port=41641/udp</span><br><span class="line">firewall-cmd --reload</span><br></pre></td></tr></table></figure><h3 id="Step-3：启动-Headscale-服务"><a href="#Step-3：启动-Headscale-服务" class="headerlink" title="Step 3：启动 Headscale 服务"></a>Step 3：启动 Headscale 服务</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> /opt/headscale</span><br><span class="line">docker compose up -d</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看启动日志</span></span><br><span class="line">docker compose logs -f headscale</span><br><span class="line"><span class="comment"># 等待 healthy 后查看 headplane</span></span><br><span class="line">docker compose logs -f headplane</span><br></pre></td></tr></table></figure><h3 id="Step-4：启动-Caddy"><a href="#Step-4：启动-Caddy" class="headerlink" title="Step 4：启动 Caddy"></a>Step 4：启动 Caddy</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 创建日志目录</span></span><br><span class="line"><span class="built_in">mkdir</span> -p /opt/caddy/data/logs</span><br><span class="line"></span><br><span class="line"><span class="built_in">cd</span> /opt/caddy</span><br><span class="line">docker compose up -d</span><br><span class="line"></span><br><span class="line"><span class="comment"># 验证</span></span><br><span class="line">docker compose ps</span><br><span class="line">docker network <span class="built_in">ls</span> | grep caddy-net</span><br></pre></td></tr></table></figure><p>也可通过页面进行验证，访问: <code>https://hs.example.com/health</code></p><h3 id="Step-5：生成-API-Key-并配置-Headplane"><a href="#Step-5：生成-API-Key-并配置-Headplane" class="headerlink" title="Step 5：生成 API Key 并配置 Headplane"></a>Step 5：生成 API Key 并配置 Headplane</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 生成 API Key</span></span><br><span class="line">docker <span class="built_in">exec</span> headscale headscale apikeys create --expiration 9999d</span><br></pre></td></tr></table></figure><p>得到一个apiKey</p><p>访问<code>https://hp.example.com/admin</code>, 可以看到下面的页面<br><img src="/static/image/2026/20260320_headplane_login.png" alt="20260320_headplane_login"></p><p>输入刚刚生成的apiKey，就可以进入页面<br><img src="/static/image/2026/20260320_headplane_admin.png" alt="20260320_headplane_admin"></p><h3 id="Step-6：创建用户"><a href="#Step-6：创建用户" class="headerlink" title="Step 6：创建用户"></a>Step 6：创建用户</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="built_in">exec</span> headscale headscale <span class="built_in">users</span> create myuser</span><br></pre></td></tr></table></figure><p>这一步也可以在headplane页面的页面上创建<br><img src="/static/image/2026/20260321_headplane_user.png" alt="20260321_headplane_user"></p><hr><h2 id="客户端接入"><a href="#客户端接入" class="headerlink" title="客户端接入"></a>客户端接入</h2><h3 id="客户端安装"><a href="#客户端安装" class="headerlink" title="客户端安装"></a>客户端安装</h3><p>MacOs:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">brew install  tailscale</span><br><span class="line">sudo tailscaled</span><br></pre></td></tr></table></figure><p>Linux:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -fsSL https://tailscale.com/install.sh | sh</span><br></pre></td></tr></table></figure><h3 id="节点接入"><a href="#节点接入" class="headerlink" title="节点接入"></a>节点接入</h3><p>接入方式一：预授权方式接入</p><ol><li>服务端生成预授权key</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="built_in">exec</span> headscale headscale preauthkeys create \</span><br><span class="line">    --user myuser_id \</span><br><span class="line">    --reusable \</span><br><span class="line">    --expiration 90d</span><br></pre></td></tr></table></figure><p>注意这里的myuser_id, 是之前创建的用户的id, 可以通过命令<code>docker exec headscale headscale nodes list</code>查看 </p><p>也可以在页面生成<br><img src="/static/image/2026/20260321_headplane_auth_key.png" alt="20260321_headplane_auth_key"></p><ol start="2"><li>在客户端执行以下命令</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> tailscale up \</span><br><span class="line">    --login-server=https://hs.example.com \</span><br><span class="line">    --authkey=&lt;预授权key&gt;</span><br></pre></td></tr></table></figure><p>方式二：交互式登录</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> tailscale up --login-server=https://hs.example.com</span><br><span class="line"><span class="comment"># 按提示在浏览器完成授权</span></span><br></pre></td></tr></table></figure><hr><h2 id="日常运维"><a href="#日常运维" class="headerlink" title="日常运维"></a>日常运维</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 查看所有节点</span></span><br><span class="line">docker <span class="built_in">exec</span> headscale headscale nodes list</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看所有用户</span></span><br><span class="line">docker <span class="built_in">exec</span> headscale headscale <span class="built_in">users</span> list</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看路由</span></span><br><span class="line">docker <span class="built_in">exec</span> headscale headscale routes list</span><br><span class="line"></span><br><span class="line"><span class="comment"># 更新镜像</span></span><br><span class="line"><span class="built_in">cd</span> /opt/caddy &amp;&amp; docker compose pull &amp;&amp; docker compose up -d</span><br><span class="line"><span class="built_in">cd</span> /opt/headscale &amp;&amp; docker compose pull &amp;&amp; docker compose up -d</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看实时日志</span></span><br><span class="line">docker compose logs -f headscale</span><br><span class="line">docker compose logs -f headplane</span><br><span class="line">docker logs caddy -f</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://stackli.me/posts/2026/headscale-deploy</id>
    <link href="https://stackli.me/posts/2026/headscale-deploy"/>
    <published>2026-03-19T14:40:51.000Z</published>
    <summary>
      <![CDATA[<p>如果你有一个NAS，或者有一个家庭服务器，你一定想过如果能够随时随地访问到家里的网络。也许你有一个云服务器，不希望暴露ssh端口到公网。<br>tailscale&#x2F;headscale就能实现我们这个需求，接入的设备可以像内网一样，轻松实现互相访问。其中tailscale需要依赖tailscale官方提供的元数据服务，负责协调和管理网络。而headscale则是其开源实现，我们使用headscale来实现完全自主的私有内容。<br>本文就介绍了使用headscale搭建一个完全自主的私有内网，并使用Caddy作为反向代理，实现域名解析和端口转发。</p>]]>
    </summary>
    <title>Headscale搭建过程实录</title>
    <updated>2026-06-02T00:00:58.014Z</updated>
  </entry>
  <entry>
    <author>
      <name>stackli</name>
    </author>
    <category term="生产力" scheme="https://stackli.me/categories/%E7%94%9F%E4%BA%A7%E5%8A%9B/"/>
    <category term="博客" scheme="https://stackli.me/tags/%E5%8D%9A%E5%AE%A2/"/>
    <category term="AI" scheme="https://stackli.me/tags/AI/"/>
    <content>
      <![CDATA[<p>自从采用hexo写博客之后，遇到需要保存截图的地方，就比较繁琐。按照正常的流程，需要</p><ol><li>使用截图软件的快捷键进行截图，这一步挺快</li><li>保存截图到指定的目录，选目录就比较麻烦</li><li>修改文件名，这一步也挺麻烦</li><li>将图片路径插入到markdown中去，这一步最麻烦，需要组装完整的访问路径。</li></ol><p>之前买了Alfred Powerpack，支持自定义工作流，我在想能不能使用Alfre，自定义一个工作流，来实现我的需求。<br>花了不到半个小时，在AI的加持下，就实现了一个自定义截图工作流：</p><ol><li>使用截图软件截图，图片会保存到剪贴板中</li><li>输入Alfred命令，将剪贴板中的图片保存到指定目录中，并在剪切板中保存markdown的图片路径格式,类似<code>![20260315_dog0](/static/image/2026/20260315_dog0.png)</code></li><li>将图片路径粘贴到markdown对应的位置，搞定</li></ol><p>我把这个工作流命名为<code>blmg</code>(blog image)，有以下功能：</p><ul><li>可以自定义图片名，如果不输入图片名，则使用 yyyyMMdd_&lt;随机4位字符串&gt; 作为文件名</li><li>文件父路径为 <code>yyyyMMdd</code>，这样按年归档，便于管理</li><li>图片会进行压缩，减少上传体积</li></ul><p>如果后期需要扩展，还可以添加其他功能，比如自动上传图片到图床等。</p><p>工作流：<br><img src="/static/image/2026/20260315_alfred_clip_workflow.png" alt="20260315_alfred_clip_workflow"></p><p>其中的脚本如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"></span><br><span class="line">input=<span class="string">&quot;<span class="variable">$1</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 博客静态目录</span></span><br><span class="line">base_dir=<span class="string">&quot;/Users/liqiao/Devlopment/blog/source/static/image&quot;</span></span><br><span class="line">year=$(<span class="built_in">date</span> +%Y)</span><br><span class="line">date_str=$(<span class="built_in">date</span> +%Y%m%d)</span><br><span class="line"></span><br><span class="line">target_dir=<span class="string">&quot;<span class="variable">$base_dir</span>/<span class="variable">$year</span>&quot;</span></span><br><span class="line">markdown_path=<span class="string">&quot;/static/image/<span class="variable">$year</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">mkdir</span> -p <span class="string">&quot;<span class="variable">$target_dir</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 文件名前缀</span></span><br><span class="line"><span class="keyword">if</span> [ -z <span class="string">&quot;<span class="variable">$input</span>&quot;</span> ]; <span class="keyword">then</span></span><br><span class="line">    rand=$(LC_ALL=C <span class="built_in">tr</span> -dc a-z0-9 &lt;/dev/urandom | <span class="built_in">head</span> -c 4)</span><br><span class="line">    filename=<span class="string">&quot;<span class="variable">$&#123;date_str&#125;</span>_<span class="variable">$&#123;rand&#125;</span>&quot;</span></span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">    filename=<span class="string">&quot;<span class="variable">$&#123;date_str&#125;</span>_<span class="variable">$input</span>&quot;</span></span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line">file_path=<span class="string">&quot;<span class="variable">$target_dir</span>/<span class="variable">$filename</span>.png&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ---- 保存剪贴板图片 ----</span></span><br><span class="line">tmp_file=$(<span class="built_in">mktemp</span> /tmp/clip_XXXX.png)</span><br><span class="line">osascript -e <span class="string">&#x27;try</span></span><br><span class="line"><span class="string">    set theImage to the clipboard as «class PNGf»</span></span><br><span class="line"><span class="string">    set thePath to POSIX file &quot;&#x27;</span><span class="string">&quot;<span class="variable">$tmp_file</span>&quot;</span><span class="string">&#x27;&quot; as text</span></span><br><span class="line"><span class="string">    set theFile to open for access file thePath with write permission</span></span><br><span class="line"><span class="string">    set eof of theFile to 0</span></span><br><span class="line"><span class="string">    write theImage to theFile</span></span><br><span class="line"><span class="string">    close access theFile</span></span><br><span class="line"><span class="string">on error errMsg</span></span><br><span class="line"><span class="string">    error errMsg</span></span><br><span class="line"><span class="string">end try&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> [ ! -f <span class="string">&quot;<span class="variable">$tmp_file</span>&quot;</span> ]; <span class="keyword">then</span></span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;Clipboard image save failed&quot;</span></span><br><span class="line">    <span class="built_in">exit</span> 1</span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">mv</span> <span class="string">&quot;<span class="variable">$tmp_file</span>&quot;</span> <span class="string">&quot;<span class="variable">$file_path</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># PNG 压缩（可选）</span></span><br><span class="line"><span class="keyword">if</span> <span class="built_in">command</span> -v pngquant &gt;/dev/null 2&gt;&amp;1; <span class="keyword">then</span></span><br><span class="line">    pngquant --force --output <span class="string">&quot;<span class="variable">$file_path</span>&quot;</span> <span class="string">&quot;<span class="variable">$file_path</span>&quot;</span></span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 文件大小</span></span><br><span class="line">filesize=$(<span class="built_in">du</span> -h <span class="string">&quot;<span class="variable">$file_path</span>&quot;</span> | awk <span class="string">&#x27;&#123;print $1&#125;&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 生成 Markdown 并复制</span></span><br><span class="line">markdown=<span class="string">&quot;![<span class="variable">$filename</span>](<span class="variable">$markdown_path</span>/<span class="variable">$filename</span>.png)&quot;</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$markdown</span>&quot;</span> | pbcopy</span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$markdown</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ---- 输出通知内容（Post Notification 使用） ----</span></span><br><span class="line"><span class="comment"># 单行，显示文件名 + 文件大小</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$filename</span> (<span class="variable">$filesize</span>)&quot;</span></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://stackli.me/posts/2026/alfred-clip-flow</id>
    <link href="https://stackli.me/posts/2026/alfred-clip-flow"/>
    <published>2026-03-15T10:48:30.000Z</published>
    <summary>
      <![CDATA[<p>自从采用hexo写博客之后，遇到需要保存截图的地方，就比较繁琐。按照正常的流程，需要</p>
<ol>
<li>使用截图软件的快捷键进行截图，这一步挺快</li>
<li>保存截图到指定的目录，选目录就比较麻烦</li>
<li>修改文件名，这一步也挺麻烦</li>
<li]]>
    </summary>
    <title>自定义Alfred截图工作流</title>
    <updated>2026-06-02T00:00:58.014Z</updated>
  </entry>
  <entry>
    <author>
      <name>stackli</name>
    </author>
    <category term="阅读" scheme="https://stackli.me/categories/%E9%98%85%E8%AF%BB/"/>
    <category term="阅读" scheme="https://stackli.me/tags/%E9%98%85%E8%AF%BB/"/>
    <content>
      <![CDATA[<blockquote><p>本文摘录于知乎: <a href="https://www.zhihu.com/question/1976120753834443272/answer/1999158496474707591">https://www.zhihu.com/question/1976120753834443272/answer/1999158496474707591</a></p></blockquote><p><strong>阅读的绝大部分意义，恰恰就在于把你读过的东西忘掉。</strong></p><p>如果你读完一本书，能把里面的情节、人物、金句记得只字不差，那你不是在阅读，你是在把自己的大脑当成一个低级的移动硬盘。</p><p>在2026年的今天，任何一个植入了本地大模型的手机都能比你记得更清楚、更准确。如果人类还要和硅基生物比拼记忆力，那不仅是愚蠢，更是进化的倒退。</p><p>很多人对阅读的理解，至今还停留在科举时代的记诵之学。觉得我背下来了，这就是我的知识。这完全是农耕文明的思维惯性。在信息匮乏的年代，占有信息本身就是一种权力。但在今天，信息是过载的，记忆是廉价的。</p><p><strong>真正的阅读，是一场对大脑皮层的暴力重塑。</strong></p><p>当你读一本关于宏观经济的书，你忘记了具体的GDP数据，忘记了某个年份的通胀率，但你留下了一种思维定式：你会开始关注周期，你会理解供需关系如何决定价格，你会在看到新闻里的政策调整时，下意识推演它对资产价格的影响。这种下意识的推演，就是所谓的直觉。</p><p>这种直觉从何而来？</p><p>它来自于你读过的书在你的大脑里经过咀嚼、消化、吸收，最后剩下来的残渣。这些残渣不再是文字，而是变成了你的神经突触，变成了你的认知结构，变成了你思考问题的默认路径。</p><p>你忘记了书的内容，是因为内容已经被拆解成了构建你认知大厦的砖瓦。你见过谁盖好了房子，还把当初的砖头编号记得清清楚楚吗？</p><p>从脑科学的底层逻辑来看，人类的大脑本质上是一个极其吝啬的能量管理者。它无时无刻不在试图节能。记忆复杂的非生存必要信息，对原始大脑来说是一种浪费。所以，</p><p><strong>遗忘是生物进化的自我保护机制，是极其高级的功能。</strong></p><p>如果一个人什么都忘不掉，他的大脑会迅速崩溃。</p><p>阅读的过程，就是你利用前额叶皮层去强行压制原始大脑的懒惰。你在阅读复杂文本时，大脑必须进行高强度逻辑运算、抽象思维和情景模拟。这是一场高强度的思维体操。</p><p>哪怕你读完就忘，在阅读的那几个小时里，你的大脑神经元进行了数以亿计的连接重组。你的逻辑推演能力变强了，你的专注时长拉长了，你对复杂信息的耐受度提高了。</p><p>这就是为什么长期阅读的人，在处理突发危机时往往更冷静。因为他们的思维带宽已经被撑大了，他们习惯处理高维复杂信息。</p><p>现在社会上充斥着一种反智论调，说读书无用，不如去搞钱。</p><p>说这种话的人，往往利用“幸存者偏差”，用个别辍学发财的案例，否定人类几千年积累的智慧传承。</p><p>未来社会只有两种人：</p><ul><li>给算法提供数据的耗材  </li><li>利用算法做决策的人</li></ul><p>你以为你忘了那些书？不，那些书让你拥有识别胡说八道的能力。</p><p>这种判断力与洞察力，需要大量深度阅读，去不断打磨你的认知颗粒度。</p><p>很多人说读不下去，读了就困。</p><p>这很正常。因为优质内容往往反人性。人性喜欢简单、确定、即时满足；好书往往复杂、模糊、需要延迟满足。</p><p>当你强迫自己读一本困难的哲学或社科书时，你在对抗本能。这种对抗，本身就是成长。</p><p>阅读还能拓展生命体验的边界。</p><p>如果不读书，你只能活一辈子；通过阅读，你可以体验一百种人生。</p><p>你读历史，看见帝国兴衰；<br>你读文学，看见人性幽暗；<br>你读哲学，看见思想边界。</p><p>这些不会立刻变成银行卡数字，但它决定你在人生重大抉择时，是随波逐流，还是走出自己的路。</p><p>我甚至认为，<strong>忘记是阅读的一部分。</strong></p><p>如果一本书你读完细节都记得，那可能只是复习已知内容。真正的好书会打碎旧认知，让你短暂茫然。</p><p>这种茫然，就是成长的阵痛。</p><p>那些能穿越周期、在危机中屹立不倒的人，几乎都有长期深度阅读的习惯。</p><p>他们读历史、哲学、生物学、物理学——<br>因为他们掌握了更底层的思维模型。</p><p>所谓：</p><p><strong>你读过的书，最后都会化作你的气质、谈吐与决策逻辑。</strong></p><p>当然，不是什么都值得读。</p><p>垃圾食品吃多了坏肚子，垃圾书读多了坏脑子。信息污染比空气污染更可怕。</p><p>一定要读经典——经受时间筛选仍然成立的思想。</p><p>读硬书——挑战你认知舒适区的书。</p><p>不要追求数量，而要追求深度。</p><p>阅读不是打卡，而是你与伟大灵魂的肉搏。</p><p>意义在于：</p><p>它让你在浮躁世界里，拥有一个真正属于自己的精神避难所。</p><p>当所有人被算法裹挟狂奔时，你能停下来；<br>当所有人为热点争吵时，你能看到荒诞；<br>当生活击倒你时，你能在文字中找到共鸣。</p><p>这种内心秩序，是阅读构建的。</p><p>未来的竞争，是认知的竞争。</p><p>AI可以替代技能，但无法替代：</p><ul><li>价值观  </li><li>审美  </li><li>对复杂人性的理解</li></ul><p>这些，恰恰是人类最后的堡垒。</p><p>如果因为怕忘记而不读书，那就是主动放弃在人工智能时代最重要的竞争壁垒。</p><p>在注意力即货币的时代，能控制注意力的人，就是超级富豪。</p><p>而阅读一本长书，是对碎片化生活的反抗。</p><p>当你读到这里，无论记住多少，这个过程本身，就是一次专注力的修复。</p><p>你夺回了对自己注意力的控制权。</p>]]>
    </content>
    <id>https://stackli.me/posts/2026/meaning-of-reading</id>
    <link href="https://stackli.me/posts/2026/meaning-of-reading"/>
    <published>2026-03-01T09:21:03.000Z</published>
    <summary>读过很多书，但后来大部分都被我忘记了，那阅读的意义是什么？ 摘录了一篇来自知乎的回答，得到了很多启发，也希望对屏幕前的你有所启发</summary>
    <title>阅读的意义</title>
    <updated>2026-06-02T00:00:58.014Z</updated>
  </entry>
  <entry>
    <author>
      <name>stackli</name>
    </author>
    <category term="区块链" scheme="https://stackli.me/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    <category term="加密货币" scheme="https://stackli.me/tags/%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81/"/>
    <content>
      <![CDATA[<p>大多数公链进行转账时需要消耗原生代币作为gas，在波场转账时需要消耗的是能量。如果能量不足，则会燃烧TRX。<br>使用波场能量租赁，可以节约大量gas费用</p><span id="more"></span><h3 id="快速能量租用"><a href="#快速能量租用" class="headerlink" title="快速能量租用"></a>快速能量租用</h3><ul><li><strong>租赁地址</strong>:  <div class="tag-plugin copy"><input class="copy-area" id="copy_1" value="TUEV6FNqgk1FLXv6S8CUbJsMtREARZofMM"><button class="copy-btn" onclick="util.copy(&quot;copy_1&quot;,&quot;复制成功&quot;)"><svg class="icon copy-btn" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M5.75 1a.75.75 0 00-.75.75v3c0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75v-3a.75.75 0 00-.75-.75h-4.5zm.75 3V2.5h3V4h-3zm-2.874-.467a.75.75 0 00-.752-1.298A1.75 1.75 0 002 3.75v9.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 13.25v-9.5a1.75 1.75 0 00-.874-1.515.75.75 0 10-.752 1.298.25.25 0 01.126.217v9.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-9.5a.25.25 0 01.126-.217z"></path></svg></button></div></li><li><strong>租赁费用</strong>: 65000能量&#x2F;3TRX</li><li><strong>租赁时长</strong>: 1小时</li></ul><p>向地址 <code>TUEV6FNqgk1FLXv6S8CUbJsMtREARZofMM</code> 转账 <code>3TRX</code>, 即可以获得转账一笔USDT的能量。 如果收款方方地址无USDT，需要消耗两笔能量，即需要转账 <code>6TRX</code></p><p>备注留空，能量自动分配到转账地址；备注填写对方地址：为他人买能量！</p><blockquote><p>[!TIP]<br>为了便于后续能量快速和安全租用，你可以将地址保存到钱包的地址薄。</p></blockquote><h3 id="为什么要进行能量租用"><a href="#为什么要进行能量租用" class="headerlink" title="为什么要进行能量租用"></a>为什么要进行能量租用</h3><p>大多数公链进行转账时需要消耗原生代币作为gas，在波场转账时需要消耗的是能量。如果能量不足，则会燃烧TRX。</p><p>在波场进行USDT转账时,不同的方式消耗的资源如下表格：</p><table><thead><tr><th>GAS费用类型</th><th>燃烧TRX</th><th>使用能量</th><th>能量租赁费用</th></tr></thead><tbody><tr><td>对方地址有USDT</td><td>7</td><td>65000</td><td>3TRX</td></tr><tr><td>对方地址无USDT</td><td>14</td><td>131000</td><td>6TRX</td></tr></tbody></table><p>使用能量进行USDT转账可以大幅减少TRX消耗。如果你没有足够的TRX质押获取能量，那么你可以通过能量租用的方式快速获得能量. 使用能量租赁方式进行USDT转账，可以节省约 <strong>70%</strong> 的转账费用</p>]]>
    </content>
    <id>https://stackli.me/blockchain/trx-energy-rent/</id>
    <link href="https://stackli.me/blockchain/trx-energy-rent/"/>
    <published>2026-02-06T12:08:36.000Z</published>
    <summary>
      <![CDATA[<p>大多数公链进行转账时需要消耗原生代币作为gas，在波场转账时需要消耗的是能量。如果能量不足，则会燃烧TRX。<br>使用波场能量租赁，可以节约大量gas费用</p>]]>
    </summary>
    <title>波场能量租用</title>
    <updated>2026-06-02T00:00:58.014Z</updated>
  </entry>
  <entry>
    <author>
      <name>stackli</name>
    </author>
    <category term="区块链" scheme="https://stackli.me/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    <category term="加密货币" scheme="https://stackli.me/tags/%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81/"/>
    <content>
      <![CDATA[<h3 id="交易所注册推荐"><a href="#交易所注册推荐" class="headerlink" title="交易所注册推荐"></a>交易所注册推荐</h3><p>使用以下推荐的链接或推荐码开户，可以获得对应的返佣奖励</p><ul><li><p>HTX(火币， 30%手续费折扣): <a href="https://www.htx.com.de/invite/zh-cn/1f?invite_code=fastdefi">https://www.htx.com.de/invite/zh-cn/1f?invite_code&#x3D;fastdefi</a>  推荐码: <code>fastdefi</code></p></li><li><p>Binance(币安, 10%手续费折扣): <a href="https://www.maxweb.cc/futures/ref/fastdefi">https://www.maxweb.cc/futures/ref/fastdefi</a>, 邀请码<code>fastdefi</code></p></li><li><p>BackPack(背包, 10%手续费折扣): <a href="https://backpack.exchange/refer/fw5dm68e">https://backpack.exchange/refer/fw5dm68e</a>, 邀请码<code>fw5dm68e</code></p></li><li><p>OKX(欧意, 20%手续费折扣): <a href="https://okx.com/join/51520553">https://okx.com/join/51520553</a>, 邀请码<code>51520553</code></p></li><li><p>Kraken(海妖, 获得10USDT奖励): <a href="https://krak.app/@STACKLI">https:&#x2F;&#x2F;krak.app&#x2F;@STACKLI</a>, 邀请码<code>STACKLI</code></p></li><li><p>Gate(20%手续费折扣): <a href="https://www.gatewebsite.com/signup/VVJAXQPFVQ?ref_type=103">https://www.gatewebsite.com/signup/VVJAXQPFVQ</a>,邀请码<code>VVJAXQPFVQ</code></p></li></ul>]]>
    </content>
    <id>https://stackli.me/blockchain/exchange-invite/</id>
    <link href="https://stackli.me/blockchain/exchange-invite/"/>
    <published>2026-02-06T11:50:36.000Z</published>
    <summary>使用推荐的链接或推荐码开户，可以获得对应的返佣奖励</summary>
    <title>加密货币交易所返佣推荐</title>
    <updated>2026-06-02T00:00:58.014Z</updated>
  </entry>
  <entry>
    <author>
      <name>stackli</name>
    </author>
    <category term="区块链" scheme="https://stackli.me/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    <category term="加密货币" scheme="https://stackli.me/tags/%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81/"/>
    <content>
      <![CDATA[<h2 id="长期波场能量-带宽收购"><a href="#长期波场能量-带宽收购" class="headerlink" title="长期波场能量&#x2F;带宽收购"></a>长期波场能量&#x2F;带宽收购</h2><p>通过代理方式，长期收购波场带宽和能量</p><p>由于市场波动，最新的能量收购地址和年化收益，会不定时波动。<br>目前的最新年华收益为: <strong>16%</strong></p><p>以上收益均是按日结算。除了以上收益，质押TRX后获得投票权，还可以对超级代表投票，获得投票收益，年化<strong>3.5%</strong>.</p><p>以上收益均是按日结算收益，因此是复利收益，换算成单例更高。</p><blockquote><p>能量&#x2F;宽带代理是否有风险？</p><p>无任何风险，质押与代理操作均由您在钱包中独立完成，代理宽带、能量资源后，你持有的TRX仍旧在你的地址中，你可以随时撤销代理。</p></blockquote><h2 id="波场质押收益示意"><a href="#波场质押收益示意" class="headerlink" title="波场质押收益示意"></a>波场质押收益示意</h2><p><img src="/static/image/2026/trx-earn.png"><br>质押TRX首选钱包：<strong>TronLink</strong></p><ul><li>质押流程：钱包内进入【质押】功能，选择质押为【能量】或者【带宽】，输入需要质押的TRX数量，确认完成质押</li><li>代理流程：钱包内进入【能量】或者【带宽】页面，可以查看到目前已代理数量和可代理的数量。点击【代理】，输入代理地址完成代理操作；点击【回收】，可以收回已经代理的资源</li><li>投票流程：钱包内进入【投票】功能，可以查看投票相关信息。点击【投票】，将投票权给收益最高的超级代理；点击【提取收益】，可以获取投票收益到钱包中，每天可以获取一次。</li></ul><h2 id="TRX价格风险对冲"><a href="#TRX价格风险对冲" class="headerlink" title="TRX价格风险对冲"></a>TRX价格风险对冲</h2><p>如果是使用USDT等其他稳定币购买TRX后进行质押，则可能面对TRX价格下跌带来的风险。此时可以选择在币安等交易所，开一个相同仓位的空单永续合约，用于对冲TRX价格下跌带来的风险。购买空单合约之后，如果TRX下跌，虽然持有的TRX价格下降，但合约价格上升，刚好抵消。</p><p>持有空单合约的另外一个好处时，我们大概率可以获取0.01%的资金费率，如果使用1倍杠杆转换成年化就是10.95%， 使用2倍杠杆就可以获得21.9%的收益， 以此类推，可以用于增强TRX质押收益。</p><p>建议使用HTX交易所进行做空合约，可以获得稳定的0.01%的资金费率，开户推荐可以点击<a href="/blockchain/exchange-invite/#htx">这里</a>, 通过推荐链接开户可以获得返佣奖励。</p>]]>
    </content>
    <id>https://stackli.me/blockchain/trx-earn/</id>
    <link href="https://stackli.me/blockchain/trx-earn/"/>
    <published>2026-02-06T11:24:36.000Z</published>
    <summary>
      <![CDATA[<h2 id="长期波场能量-带宽收购"><a href="#长期波场能量-带宽收购" class="headerlink"]]>
    </summary>
    <title>持有TRX获得稳定收益</title>
    <updated>2026-06-02T00:00:58.014Z</updated>
  </entry>
  <entry>
    <author>
      <name>stackli</name>
    </author>
    <category term="区块链" scheme="https://stackli.me/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    <category term="加密货币" scheme="https://stackli.me/tags/%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81/"/>
    <content>
      <![CDATA[<p>在大部分的区块链网络中进行交易，通常消耗的GAS都是消耗原生代币。但在波场中的交易，却是消耗<strong>能量</strong>和<strong>带宽</strong>。阅读完本文之后，你将了解什么是能量和带宽，能量和带宽分别的使用场景，如何获得能量和带宽</p><span id="more"></span><p>在大部分的区块链网络中进行交易，通常消耗的GAS都是消耗原生代币。但在波场中的交易，却是消耗<strong>能量</strong>和<strong>带宽</strong>。阅读完本文之后，你将了解：</p><ul><li>什么是能量和带宽</li><li>能量和带宽分别的使用场景</li><li>如何获得能量和带宽</li></ul><h2 id="带宽"><a href="#带宽" class="headerlink" title="带宽"></a>带宽</h2><p>任何交易都会消耗宽带。宽带顾名思义，其实就是对网络资源的占用。一笔交易消耗的宽带&#x3D;交易的字节数*宽带费率，当前的宽带费率为1. 因此如果一笔交易的字节数为250，则会消耗250单位的宽带。</p><p>宽带有三种获取方式：</p><ul><li>系统每日会赠送免费600单位的宽带</li><li>质押TRX为获取宽带。额度 &#x3D; (为获取Bandwidth Points质押的TRX &#x2F; 整个网络为获取Bandwidth Points质押的TRX 总额) * (Bandwidth Points上界)。 Bandwidth Points上界当前值为43,200,000,000。 Bandwidth Points上界可能随着后期的网络提案进行修改</li><li>来自其他账户的宽带代理。 质押获得的宽带除了可以自己使用之外，也可以代理给其他地址使用，当然也可以接受其他地址的代理</li></ul><p>在宽带消耗后，会在24小时之内逐步恢复</p><h2 id="能量"><a href="#能量" class="headerlink" title="能量"></a>能量</h2><p>任何交易都会消耗带宽，但执行智能合约除了消耗带宽，还需要消耗能量。能量代表执行智能合约时，对系统资源的消耗。<br>目前在进行USDT转账时，如果收款方地址有USDT，则需要消耗65,000能量；如果收款方地址无USDT，则需要消耗130,000能量。</p><p>能量获得有2中方式：</p><ul><li>质押TRX为获取能量。通过质押TRX获取的Energy 额度 &#x3D; (为获取Energy质押的TRX &#x2F; 整个网络为获取Energy质押的TRX 总额) * 能量上界，也就是所有用户按质押的TRX数量平分固定额度的Energy能量(能量上界)。目前的能量上界时180,000,000,000，也是可以跟随网络提案进行修改。</li><li>同宽带一样，也可以来自其他账户的能量代理</li></ul><p>和宽带不一样的点是，能量并不存在免费的额度。在能量消耗后，会在24小时之内逐步恢复</p>]]>
    </content>
    <id>https://stackli.me/blockchain/trx-resource/</id>
    <link href="https://stackli.me/blockchain/trx-resource/"/>
    <published>2026-01-25T12:24:36.000Z</published>
    <summary>
      <![CDATA[<p>在大部分的区块链网络中进行交易，通常消耗的GAS都是消耗原生代币。但在波场中的交易，却是消耗<strong>能量</strong>和<strong>带宽</strong>。阅读完本文之后，你将了解什么是能量和带宽，能量和带宽分别的使用场景，如何获得能量和带宽</p>]]>
    </summary>
    <title>波场的资源模型-能量和带宽</title>
    <updated>2026-06-02T00:00:58.014Z</updated>
  </entry>
  <entry>
    <author>
      <name>stackli</name>
    </author>
    <category term="博客" scheme="https://stackli.me/tags/%E5%8D%9A%E5%AE%A2/"/>
    <content>
      <![CDATA[<p>两年之前就搭建好了这个博客，但是一直没更新，最近又想更新一下，就想记录一下博客搭建的过程以及发布新博客的步骤，做一个备忘。</p><p>我的博客使用的主题是<a href="https://xaoxuu.com/wiki/stellar/">stellar</a>.</p><h2 id="博客初始化"><a href="#博客初始化" class="headerlink" title="博客初始化"></a>博客初始化</h2><h3 id="搭建博客"><a href="#搭建博客" class="headerlink" title="搭建博客"></a>搭建博客</h3><ol><li>安装hexo: <code>npm install hexo-cli -g</code></li><li>创建博客: <code>hexo init blog</code></li><li>进入博客文件夹: <code>npm i hexo-theme-stellar</code></li><li>在 blog&#x2F;_config.yml 文件中找到并修改: <code>theme: stellar</code></li></ol><h3 id="部署博客"><a href="#部署博客" class="headerlink" title="部署博客"></a>部署博客</h3><ol><li>创建一个 github 仓库，将生成的静态文件push到<code>gh-pages</code>分支</li><li>在Cloudflare中 Workders&amp;Pages创建一个Application, 关联到github仓库</li><li>配置应用时，deploy命令为: <code>npx wrangler deploy --assets=./ --compatibility-date 2026-01-31</code></li></ol><h3 id="添加新博客"><a href="#添加新博客" class="headerlink" title="添加新博客"></a>添加新博客</h3><p>创建新博客文件: <code>hexo new [layout] &lt;title&gt;</code>, 其中layout是可选的<br>预览新博客: <code>hexo s</code><br>动态刷新预览: <code>hexo clean &amp;&amp; hexo s -w</code><br>生成静态文件: <code>hexo g</code><br>发布新博客: <code>hexo d</code></p><h2 id="更新日志"><a href="#更新日志" class="headerlink" title="更新日志"></a>更新日志</h2><h3 id="2026-05-15"><a href="#2026-05-15" class="headerlink" title="2026-05-15"></a>2026-05-15</h3><p>新加博客后，需要通过将文件添加到git仓库，并push到远程分支，这个过程还挺麻烦，就搞了个脚本，运行之后自动提交并push</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/usr/bin/env bash</span></span><br><span class="line"><span class="comment"># Stage changes under source/, commit with timestamp message, push.</span></span><br><span class="line"><span class="built_in">set</span> -euo pipefail</span><br><span class="line"><span class="built_in">cd</span> <span class="string">&quot;<span class="subst">$(dirname <span class="string">&quot;<span class="variable">$0</span>&quot;</span>)</span>/..&quot;</span></span><br><span class="line"></span><br><span class="line">git add -- <span class="built_in">source</span>/</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> git diff --cached --quiet; <span class="keyword">then</span></span><br><span class="line">  <span class="built_in">echo</span> <span class="string">&quot;Nothing to commit under source/&quot;</span></span><br><span class="line">  <span class="built_in">exit</span> 0</span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line">git commit -m <span class="string">&quot;<span class="subst">$(date &#x27;+%Y-%m-%d %H:%M:%S&#x27;)</span>&quot;</span></span><br><span class="line">git push</span><br><span class="line"></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://stackli.me/posts/2026/blog-note</id>
    <link href="https://stackli.me/posts/2026/blog-note"/>
    <published>2026-01-24T06:07:05.000Z</published>
    <summary>
      <![CDATA[<p>两年之前就搭建好了这个博客，但是一直没更新，最近又想更新一下，就想记录一下博客搭建的过程以及发布新博客的步骤，做一个备忘。</p>
<p>我的博客使用的主题是<a]]>
    </summary>
    <title>博客搭建备忘</title>
    <updated>2026-06-02T00:00:58.014Z</updated>
  </entry>
  <entry>
    <author>
      <name>stackli</name>
    </author>
    <category term="系统与网络" scheme="https://stackli.me/categories/%E7%B3%BB%E7%BB%9F%E4%B8%8E%E7%BD%91%E7%BB%9C/"/>
    <category term="网络代理" scheme="https://stackli.me/tags/%E7%BD%91%E7%BB%9C%E4%BB%A3%E7%90%86/"/>
    <content>
      <![CDATA[<p>在上一篇博客中，我们了解了几种常见的网络代理。本文将通过python代码来实现网络代理。代码仅做学习研究使用，不进行性能考虑。</p><h2 id="HTTP代理"><a href="#HTTP代理" class="headerlink" title="HTTP代理"></a>HTTP代理</h2><p>在上一篇博客中我们了解到，http代理分为普通代理和隧道代理。如果目标服务器是http服务，则使用普通代理；如果目标服务器是https服务，则使用隧道代理。</p><p>以下是一个简单的http代理实现</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#!/usr/bin/env python3</span></span><br><span class="line"><span class="comment"># -*- coding: utf-8 -*-</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> socket</span><br><span class="line"><span class="keyword">import</span> select</span><br><span class="line"><span class="keyword">import</span> logging</span><br><span class="line"><span class="keyword">import</span> shutil</span><br><span class="line"><span class="keyword">from</span> http.server <span class="keyword">import</span> ThreadingHTTPServer, BaseHTTPRequestHandler</span><br><span class="line"><span class="keyword">from</span> urllib.parse <span class="keyword">import</span> urlparse</span><br><span class="line"></span><br><span class="line"><span class="comment"># 配置日志</span></span><br><span class="line">logging.basicConfig(</span><br><span class="line">    level=logging.INFO,</span><br><span class="line">    <span class="built_in">format</span>=<span class="string">&#x27;%(asctime)s - %(levelname)s - %(message)s&#x27;</span></span><br><span class="line">)</span><br><span class="line">logger = logging.getLogger(__name__)</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">ProxyHandler</span>(<span class="title class_ inherited__">BaseHTTPRequestHandler</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">do_GET</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;处理 GET 请求&quot;&quot;&quot;</span></span><br><span class="line">        <span class="variable language_">self</span>._handle_request(<span class="string">&#x27;GET&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">do_POST</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;处理 POST 请求&quot;&quot;&quot;</span></span><br><span class="line">        <span class="variable language_">self</span>._handle_request(<span class="string">&#x27;POST&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">do_PUT</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;处理 PUT 请求&quot;&quot;&quot;</span></span><br><span class="line">        <span class="variable language_">self</span>._handle_request(<span class="string">&#x27;PUT&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">do_DELETE</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;处理 DELETE 请求&quot;&quot;&quot;</span></span><br><span class="line">        <span class="variable language_">self</span>._handle_request(<span class="string">&#x27;DELETE&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">do_HEAD</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;处理 HEAD 请求&quot;&quot;&quot;</span></span><br><span class="line">        <span class="variable language_">self</span>._handle_request(<span class="string">&#x27;HEAD&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">do_OPTIONS</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;处理 OPTIONS 请求&quot;&quot;&quot;</span></span><br><span class="line">        <span class="variable language_">self</span>._handle_request(<span class="string">&#x27;OPTIONS&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">do_PATCH</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;处理 PATCH 请求&quot;&quot;&quot;</span></span><br><span class="line">        <span class="variable language_">self</span>._handle_request(<span class="string">&#x27;PATCH&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">_handle_request</span>(<span class="params">self, method</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;通用请求处理方法&quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            <span class="comment"># 解析目标URL</span></span><br><span class="line">            url = urlparse(<span class="variable language_">self</span>.path)</span><br><span class="line">            <span class="keyword">if</span> <span class="keyword">not</span> url.netloc:</span><br><span class="line">                <span class="variable language_">self</span>.send_error(<span class="number">400</span>, <span class="string">&quot;Bad Request&quot;</span>)</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">            <span class="comment"># 创建到目标服务器的连接</span></span><br><span class="line">            target_host = url.netloc</span><br><span class="line">            target_port = <span class="number">80</span></span><br><span class="line">            <span class="keyword">if</span> <span class="string">&#x27;:&#x27;</span> <span class="keyword">in</span> target_host:</span><br><span class="line">                target_host, target_port = target_host.split(<span class="string">&#x27;:&#x27;</span>)</span><br><span class="line">                target_port = <span class="built_in">int</span>(target_port)</span><br><span class="line"></span><br><span class="line">            <span class="comment"># 建立连接</span></span><br><span class="line">            target_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)</span><br><span class="line">            target_sock.connect((target_host, target_port))</span><br><span class="line"></span><br><span class="line">            <span class="comment"># 读取请求体</span></span><br><span class="line">            content_length = <span class="built_in">int</span>(<span class="variable language_">self</span>.headers.get(<span class="string">&#x27;Content-Length&#x27;</span>, <span class="number">0</span>))</span><br><span class="line">            body = <span class="variable language_">self</span>.rfile.read(content_length) <span class="keyword">if</span> content_length &gt; <span class="number">0</span> <span class="keyword">else</span> <span class="string">b&#x27;&#x27;</span></span><br><span class="line"></span><br><span class="line">            <span class="comment"># 构建请求头</span></span><br><span class="line">            request_headers = []</span><br><span class="line">            <span class="keyword">for</span> header, value <span class="keyword">in</span> <span class="variable language_">self</span>.headers.items():</span><br><span class="line">                <span class="keyword">if</span> header.lower() <span class="keyword">not</span> <span class="keyword">in</span> (<span class="string">&#x27;proxy-connection&#x27;</span>, <span class="string">&#x27;connection&#x27;</span>, <span class="string">&#x27;host&#x27;</span>):</span><br><span class="line">                    request_headers.append(<span class="string">f&quot;<span class="subst">&#123;header&#125;</span>: <span class="subst">&#123;value&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line">            <span class="comment"># 发送请求到目标服务器</span></span><br><span class="line">            request = <span class="string">f&quot;<span class="subst">&#123;method&#125;</span> <span class="subst">&#123;self.path&#125;</span> HTTP/1.1\r\n&quot;</span></span><br><span class="line">            request += <span class="string">f&quot;Host: <span class="subst">&#123;target_host&#125;</span>\r\n&quot;</span></span><br><span class="line">            request += <span class="string">&quot;Connection: close\r\n&quot;</span></span><br><span class="line">            request += <span class="string">&quot;\r\n&quot;</span>.join(request_headers)</span><br><span class="line">            request += <span class="string">&quot;\r\n&quot;</span></span><br><span class="line">            <span class="keyword">if</span> body:</span><br><span class="line">                request += <span class="string">f&quot;Content-Length: <span class="subst">&#123;<span class="built_in">len</span>(body)&#125;</span>\r\n&quot;</span></span><br><span class="line">            request += <span class="string">&quot;\r\n&quot;</span></span><br><span class="line">            </span><br><span class="line">            <span class="comment"># 发送请求头和请求体</span></span><br><span class="line">            target_sock.send(request.encode())</span><br><span class="line">            <span class="keyword">if</span> body:</span><br><span class="line">                target_sock.send(body)</span><br><span class="line"></span><br><span class="line">            <span class="comment"># 使用shutil.copyfileobj进行数据传输</span></span><br><span class="line">            target_file = target_sock.makefile(<span class="string">&#x27;rb&#x27;</span>)</span><br><span class="line">            shutil.copyfileobj(target_file, <span class="variable language_">self</span>.wfile, length=<span class="number">8192</span>)</span><br><span class="line">            target_file.close()</span><br><span class="line">            target_sock.close()</span><br><span class="line"></span><br><span class="line">        <span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line">            logger.error(<span class="string">f&quot;处理 <span class="subst">&#123;method&#125;</span> 请求时发生错误: <span class="subst">&#123;e&#125;</span>&quot;</span>)</span><br><span class="line">            <span class="variable language_">self</span>.send_error(<span class="number">500</span>, <span class="string">f&quot;服务器内部错误: <span class="subst">&#123;<span class="built_in">str</span>(e)&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">do_CONNECT</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;处理 CONNECT 请求（隧道代理）&quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            <span class="comment"># 解析目标地址</span></span><br><span class="line">            target_host, target_port = <span class="variable language_">self</span>.path.split(<span class="string">&#x27;:&#x27;</span>)</span><br><span class="line">            target_port = <span class="built_in">int</span>(target_port)</span><br><span class="line"></span><br><span class="line">            <span class="comment"># 创建到目标服务器的连接</span></span><br><span class="line">            target_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)</span><br><span class="line">            target_sock.connect((target_host, target_port))</span><br><span class="line"></span><br><span class="line">            <span class="comment"># 发送连接成功响应</span></span><br><span class="line">            <span class="variable language_">self</span>.send_response(<span class="number">200</span>, <span class="string">&#x27;Connection Established&#x27;</span>)</span><br><span class="line">            <span class="variable language_">self</span>.send_header(<span class="string">&#x27;Proxy-Agent&#x27;</span>, <span class="string">&#x27;Python-Proxy&#x27;</span>)</span><br><span class="line">            <span class="variable language_">self</span>.end_headers()</span><br><span class="line"></span><br><span class="line">            <span class="comment"># 获取客户端socket</span></span><br><span class="line">            client_sock = <span class="variable language_">self</span>.connection</span><br><span class="line"></span><br><span class="line">            <span class="comment"># 设置非阻塞模式</span></span><br><span class="line">            client_sock.setblocking(<span class="literal">False</span>)</span><br><span class="line">            target_sock.setblocking(<span class="literal">False</span>)</span><br><span class="line"></span><br><span class="line">            <span class="comment"># 使用select进行双向转发</span></span><br><span class="line">            <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">                <span class="comment"># 使用select监控两个socket</span></span><br><span class="line">                readable, _, exceptional = select.select(</span><br><span class="line">                    [client_sock, target_sock],</span><br><span class="line">                    [],</span><br><span class="line">                    [client_sock, target_sock],</span><br><span class="line">                    <span class="number">1</span></span><br><span class="line">                )</span><br><span class="line"></span><br><span class="line">                <span class="comment"># 检查是否有异常</span></span><br><span class="line">                <span class="keyword">if</span> exceptional:</span><br><span class="line">                    <span class="keyword">break</span></span><br><span class="line"></span><br><span class="line">                <span class="comment"># 处理可读的socket</span></span><br><span class="line">                <span class="keyword">for</span> sock <span class="keyword">in</span> readable:</span><br><span class="line">                    <span class="keyword">try</span>:</span><br><span class="line">                        <span class="comment"># 确定源和目标socket</span></span><br><span class="line">                        <span class="keyword">if</span> sock <span class="keyword">is</span> client_sock:</span><br><span class="line">                            source = client_sock</span><br><span class="line">                            target = target_sock</span><br><span class="line">                        <span class="keyword">else</span>:</span><br><span class="line">                            source = target_sock</span><br><span class="line">                            target = client_sock</span><br><span class="line"></span><br><span class="line">                        <span class="comment"># 读取数据</span></span><br><span class="line">                        data = source.recv(<span class="number">8192</span>)</span><br><span class="line">                        <span class="keyword">if</span> <span class="keyword">not</span> data:</span><br><span class="line">                            <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">                        <span class="comment"># 发送数据</span></span><br><span class="line">                        target.send(data)</span><br><span class="line"></span><br><span class="line">                    <span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line">                        logger.error(<span class="string">f&quot;隧道连接发生错误: <span class="subst">&#123;e&#125;</span>&quot;</span>)</span><br><span class="line">                        <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line">            logger.error(<span class="string">f&quot;处理 CONNECT 请求时发生错误: <span class="subst">&#123;e&#125;</span>&quot;</span>)</span><br><span class="line">            <span class="variable language_">self</span>.send_error(<span class="number">500</span>, <span class="string">f&quot;服务器内部错误: <span class="subst">&#123;<span class="built_in">str</span>(e)&#125;</span>&quot;</span>)</span><br><span class="line">        <span class="keyword">finally</span>:</span><br><span class="line">            <span class="comment"># 确保连接被关闭</span></span><br><span class="line">            <span class="keyword">try</span>:</span><br><span class="line">                client_sock.close()</span><br><span class="line">            <span class="keyword">except</span>:</span><br><span class="line">                <span class="keyword">pass</span></span><br><span class="line">            <span class="keyword">try</span>:</span><br><span class="line">                target_sock.close()</span><br><span class="line">            <span class="keyword">except</span>:</span><br><span class="line">                <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">log_message</span>(<span class="params">self, <span class="built_in">format</span>, *args</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;自定义日志格式&quot;&quot;&quot;</span></span><br><span class="line">        logger.info(<span class="string">f&quot;<span class="subst">&#123;self.address_string()&#125;</span> - <span class="subst">&#123;<span class="built_in">format</span> % args&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">run_proxy</span>(<span class="params">host=<span class="string">&#x27;0.0.0.0&#x27;</span>, port=<span class="number">9000</span></span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;运行代理服务器&quot;&quot;&quot;</span></span><br><span class="line">    server_address = (host, port)</span><br><span class="line">    httpd = ThreadingHTTPServer(server_address, ProxyHandler)</span><br><span class="line">    logger.info(<span class="string">f&quot;代理服务器启动于 <span class="subst">&#123;host&#125;</span>:<span class="subst">&#123;port&#125;</span>&quot;</span>)</span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        httpd.serve_forever()</span><br><span class="line">    <span class="keyword">except</span> KeyboardInterrupt:</span><br><span class="line">        logger.info(<span class="string">&quot;正在关闭代理服务器&quot;</span>)</span><br><span class="line">        httpd.server_close()</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&#x27;__main__&#x27;</span>:</span><br><span class="line">    run_proxy()</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="SCOKS代理"><a href="#SCOKS代理" class="headerlink" title="SCOKS代理"></a>SCOKS代理</h2><p>以下是一个简单的socks代理实现</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br><span class="line">216</span><br><span class="line">217</span><br><span class="line">218</span><br><span class="line">219</span><br><span class="line">220</span><br><span class="line">221</span><br><span class="line">222</span><br><span class="line">223</span><br><span class="line">224</span><br><span class="line">225</span><br><span class="line">226</span><br><span class="line">227</span><br><span class="line">228</span><br><span class="line">229</span><br><span class="line">230</span><br><span class="line">231</span><br><span class="line">232</span><br><span class="line">233</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> socket</span><br><span class="line"><span class="keyword">import</span> select</span><br><span class="line"><span class="keyword">import</span> logging</span><br><span class="line"><span class="keyword">import</span> struct</span><br><span class="line"><span class="keyword">from</span> socketserver <span class="keyword">import</span> ThreadingTCPServer, StreamRequestHandler</span><br><span class="line"></span><br><span class="line"><span class="comment"># 配置日志</span></span><br><span class="line">logging.basicConfig(</span><br><span class="line">    level=logging.INFO,</span><br><span class="line">    <span class="built_in">format</span>=<span class="string">&#x27;%(asctime)s - %(levelname)s - %(message)s&#x27;</span></span><br><span class="line">)</span><br><span class="line">logger = logging.getLogger(__name__)</span><br><span class="line"></span><br><span class="line"><span class="comment"># SOCKS5 协议常量</span></span><br><span class="line">SOCKS_VERSION = <span class="number">5</span></span><br><span class="line">CONNECT = <span class="number">1</span></span><br><span class="line">BIND = <span class="number">2</span></span><br><span class="line">UDP_ASSOCIATE = <span class="number">3</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 地址类型</span></span><br><span class="line">ATYP_IPV4 = <span class="number">1</span></span><br><span class="line">ATYP_DOMAINNAME = <span class="number">3</span></span><br><span class="line">ATYP_IPV6 = <span class="number">4</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">SocksProxy</span>(<span class="title class_ inherited__">StreamRequestHandler</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">handle</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;处理SOCKS5连接请求&quot;&quot;&quot;</span></span><br><span class="line">        <span class="comment"># 处理握手</span></span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> <span class="variable language_">self</span>.handle_handshake():</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        </span><br><span class="line">        <span class="comment"># 处理连接请求</span></span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> <span class="variable language_">self</span>.handle_connect():</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">handle_handshake</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;处理SOCKS5握手&quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            <span class="comment"># 读取客户端支持的认证方法</span></span><br><span class="line">            version = <span class="variable language_">self</span>.connection.recv(<span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> <span class="keyword">not</span> version <span class="keyword">or</span> <span class="built_in">ord</span>(version) != SOCKS_VERSION:</span><br><span class="line">                logger.error(<span class="string">f&quot;不支持的SOCKS版本: <span class="subst">&#123;version&#125;</span>&quot;</span>)</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line"></span><br><span class="line">            <span class="comment"># 读取认证方法数量</span></span><br><span class="line">            nmethods = <span class="variable language_">self</span>.connection.recv(<span class="number">1</span>)</span><br><span class="line">            <span class="keyword">if</span> <span class="keyword">not</span> nmethods:</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line">            </span><br><span class="line">            <span class="comment"># 读取所有支持的认证方法</span></span><br><span class="line">            methods = <span class="variable language_">self</span>.connection.recv(<span class="built_in">ord</span>(nmethods))</span><br><span class="line">            <span class="keyword">if</span> <span class="keyword">not</span> methods:</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line"></span><br><span class="line">            <span class="comment"># 目前我们不需要认证，直接返回 NO AUTHENTICATION REQUIRED (0x00)</span></span><br><span class="line">            <span class="variable language_">self</span>.connection.sendall(struct.pack(<span class="string">&#x27;!BB&#x27;</span>, SOCKS_VERSION, <span class="number">0</span>))</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">True</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line">            logger.error(<span class="string">f&quot;握手阶段错误: <span class="subst">&#123;e&#125;</span>&quot;</span>)</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">handle_connect</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;处理SOCKS5连接请求&quot;&quot;&quot;</span></span><br><span class="line">        remote = <span class="literal">None</span></span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            <span class="comment"># 读取版本和命令</span></span><br><span class="line">            version, cmd, _, address_type = struct.unpack(<span class="string">&#x27;!BBBB&#x27;</span>, <span class="variable language_">self</span>.connection.recv(<span class="number">4</span>))</span><br><span class="line">            </span><br><span class="line">            <span class="keyword">if</span> version != SOCKS_VERSION:</span><br><span class="line">                logger.error(<span class="string">f&quot;不支持的SOCKS版本: <span class="subst">&#123;version&#125;</span>&quot;</span>)</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line"></span><br><span class="line">            <span class="comment"># 目前只支持CONNECT命令</span></span><br><span class="line">            <span class="keyword">if</span> cmd != CONNECT:</span><br><span class="line">                logger.error(<span class="string">f&quot;不支持的命令: <span class="subst">&#123;cmd&#125;</span>&quot;</span>)</span><br><span class="line">                <span class="variable language_">self</span>.send_reply(<span class="number">7</span>)  <span class="comment"># Command not supported</span></span><br><span class="line">                <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line"></span><br><span class="line">            <span class="comment"># 解析目标地址</span></span><br><span class="line">            <span class="keyword">if</span> address_type == ATYP_IPV4:</span><br><span class="line">                <span class="comment"># IPv4</span></span><br><span class="line">                target_addr = socket.inet_ntoa(<span class="variable language_">self</span>.connection.recv(<span class="number">4</span>))</span><br><span class="line">            <span class="keyword">elif</span> address_type == ATYP_DOMAINNAME:</span><br><span class="line">                <span class="comment"># 域名</span></span><br><span class="line">                domain_length = <span class="built_in">ord</span>(<span class="variable language_">self</span>.connection.recv(<span class="number">1</span>))</span><br><span class="line">                target_addr = <span class="variable language_">self</span>.connection.recv(domain_length).decode()</span><br><span class="line">            <span class="keyword">elif</span> address_type == ATYP_IPV6:</span><br><span class="line">                <span class="comment"># IPv6</span></span><br><span class="line">                target_addr = socket.inet_ntop(socket.AF_INET6, <span class="variable language_">self</span>.connection.recv(<span class="number">16</span>))</span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                logger.error(<span class="string">f&quot;不支持的地址类型: <span class="subst">&#123;address_type&#125;</span>&quot;</span>)</span><br><span class="line">                <span class="variable language_">self</span>.send_reply(<span class="number">8</span>)  <span class="comment"># Address type not supported</span></span><br><span class="line">                <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line"></span><br><span class="line">            <span class="comment"># 读取端口号</span></span><br><span class="line">            target_port = struct.unpack(<span class="string">&#x27;!H&#x27;</span>, <span class="variable language_">self</span>.connection.recv(<span class="number">2</span>))[<span class="number">0</span>]</span><br><span class="line"></span><br><span class="line">            <span class="comment"># 尝试连接到目标服务器</span></span><br><span class="line">            <span class="keyword">try</span>:</span><br><span class="line">                <span class="keyword">if</span> address_type == ATYP_DOMAINNAME:</span><br><span class="line">                    remote = socket.create_connection((target_addr, target_port), timeout=<span class="number">10</span>)</span><br><span class="line">                <span class="keyword">else</span>:</span><br><span class="line">                    remote = socket.socket(socket.AF_INET <span class="keyword">if</span> address_type == ATYP_IPV4 <span class="keyword">else</span> socket.AF_INET6)</span><br><span class="line">                    remote.settimeout(<span class="number">10</span>)</span><br><span class="line">                    remote.connect((target_addr, target_port))</span><br><span class="line"></span><br><span class="line">                bind_address = remote.getsockname()</span><br><span class="line">                logger.info(<span class="string">f&quot;已连接到 <span class="subst">&#123;target_addr&#125;</span>:<span class="subst">&#123;target_port&#125;</span>&quot;</span>)</span><br><span class="line">                </span><br><span class="line">                <span class="comment"># 发送成功响应</span></span><br><span class="line">                <span class="variable language_">self</span>.send_reply(<span class="number">0</span>, bind_address[<span class="number">0</span>], bind_address[<span class="number">1</span>])</span><br><span class="line"></span><br><span class="line">            <span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line">                logger.error(<span class="string">f&quot;连接目标服务器失败: <span class="subst">&#123;e&#125;</span>&quot;</span>)</span><br><span class="line">                <span class="variable language_">self</span>.send_reply(<span class="number">4</span>)  <span class="comment"># Host unreachable</span></span><br><span class="line">                <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line"></span><br><span class="line">            <span class="comment"># 开始转发数据</span></span><br><span class="line">            <span class="variable language_">self</span>.exchange_loop(<span class="variable language_">self</span>.connection, remote)</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">True</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line">            logger.error(<span class="string">f&quot;处理连接请求时发生错误: <span class="subst">&#123;e&#125;</span>&quot;</span>)</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line">        <span class="keyword">finally</span>:</span><br><span class="line">            <span class="keyword">if</span> remote:</span><br><span class="line">                <span class="keyword">try</span>:</span><br><span class="line">                    remote.close()</span><br><span class="line">                <span class="keyword">except</span>:</span><br><span class="line">                    <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">send_reply</span>(<span class="params">self, reply_code, bind_addr=<span class="string">&#x27;0.0.0.0&#x27;</span>, bind_port=<span class="number">0</span></span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;发送SOCKS5响应&quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            <span class="comment"># 将IP地址转换为网络字节序</span></span><br><span class="line">            <span class="keyword">if</span> <span class="string">&#x27;:&#x27;</span> <span class="keyword">in</span> bind_addr:  <span class="comment"># IPv6</span></span><br><span class="line">                addr_bytes = socket.inet_pton(socket.AF_INET6, bind_addr)</span><br><span class="line">                addr_type = ATYP_IPV6</span><br><span class="line">            <span class="keyword">else</span>:  <span class="comment"># IPv4</span></span><br><span class="line">                addr_bytes = socket.inet_aton(bind_addr)</span><br><span class="line">                addr_type = ATYP_IPV4</span><br><span class="line"></span><br><span class="line">            reply = struct.pack(<span class="string">&#x27;!BBBB&#x27;</span>, SOCKS_VERSION, reply_code, <span class="number">0</span>, addr_type)</span><br><span class="line">            reply += addr_bytes + struct.pack(<span class="string">&#x27;!H&#x27;</span>, bind_port)</span><br><span class="line">            <span class="variable language_">self</span>.connection.sendall(reply)</span><br><span class="line">        <span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line">            logger.error(<span class="string">f&quot;发送响应时发生错误: <span class="subst">&#123;e&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">exchange_loop</span>(<span class="params">self, client, remote</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;数据交换循环&quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            client.setblocking(<span class="literal">False</span>)</span><br><span class="line">            remote.setblocking(<span class="literal">False</span>)</span><br><span class="line"></span><br><span class="line">            <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">                <span class="keyword">try</span>:</span><br><span class="line">                    r, w, e = select.select([client, remote], [], [], <span class="number">0.5</span>)</span><br><span class="line">                <span class="keyword">except</span>:</span><br><span class="line">                    <span class="keyword">break</span></span><br><span class="line"></span><br><span class="line">                <span class="keyword">if</span> e:</span><br><span class="line">                    <span class="keyword">break</span></span><br><span class="line"></span><br><span class="line">                <span class="keyword">for</span> sock <span class="keyword">in</span> r:</span><br><span class="line">                    <span class="keyword">try</span>:</span><br><span class="line">                        data = sock.recv(<span class="number">32768</span>)</span><br><span class="line">                        <span class="keyword">if</span> <span class="keyword">not</span> data:</span><br><span class="line">                            <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">                        <span class="keyword">if</span> sock <span class="keyword">is</span> client:</span><br><span class="line">                            remote.setblocking(<span class="literal">True</span>)</span><br><span class="line">                            <span class="keyword">try</span>:</span><br><span class="line">                                remote.sendall(data)</span><br><span class="line">                            <span class="keyword">finally</span>:</span><br><span class="line">                                remote.setblocking(<span class="literal">False</span>)</span><br><span class="line">                        <span class="keyword">else</span>:</span><br><span class="line">                            client.setblocking(<span class="literal">True</span>)</span><br><span class="line">                            <span class="keyword">try</span>:</span><br><span class="line">                                client.sendall(data)</span><br><span class="line">                            <span class="keyword">finally</span>:</span><br><span class="line">                                client.setblocking(<span class="literal">False</span>)</span><br><span class="line">                    <span class="keyword">except</span>:</span><br><span class="line">                        <span class="keyword">return</span></span><br><span class="line">        <span class="keyword">finally</span>:</span><br><span class="line">            <span class="keyword">for</span> sock <span class="keyword">in</span> [client, remote]:</span><br><span class="line">                <span class="keyword">try</span>:</span><br><span class="line">                    sock.close()</span><br><span class="line">                <span class="keyword">except</span>:</span><br><span class="line">                    <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">ThreadingSocksServer</span>(<span class="title class_ inherited__">ThreadingTCPServer</span>):</span><br><span class="line">    allow_reuse_address = <span class="literal">True</span></span><br><span class="line">    daemon_threads = <span class="literal">True</span>  <span class="comment"># 设置为守护线程，这样主线程退出时它们会自动退出</span></span><br><span class="line">    </span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">server_close</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="string">&quot;&quot;&quot;确保服务器正确关闭&quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            <span class="variable language_">self</span>.socket.shutdown(socket.SHUT_RDWR)</span><br><span class="line">        <span class="keyword">except</span>:</span><br><span class="line">            <span class="keyword">pass</span></span><br><span class="line">        <span class="variable language_">self</span>.socket.close()</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">run_proxy</span>(<span class="params">host=<span class="string">&#x27;0.0.0.0&#x27;</span>, port=<span class="number">9001</span></span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;运行SOCKS5代理服务器&quot;&quot;&quot;</span></span><br><span class="line">    server = <span class="literal">None</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">shutdown_server</span>(<span class="params">signum=<span class="literal">None</span>, frame=<span class="literal">None</span></span>):</span><br><span class="line">        <span class="keyword">nonlocal</span> server</span><br><span class="line">        <span class="keyword">if</span> server:</span><br><span class="line">            logger.info(<span class="string">&quot;正在关闭SOCKS5代理服务器...&quot;</span>)</span><br><span class="line">            <span class="keyword">try</span>:</span><br><span class="line">                server.server_close()  <span class="comment"># 关闭服务器和所有连接</span></span><br><span class="line">            <span class="keyword">except</span>:</span><br><span class="line">                <span class="keyword">pass</span></span><br><span class="line">            logger.info(<span class="string">&quot;服务器已关闭&quot;</span>)</span><br><span class="line">            <span class="keyword">import</span> sys</span><br><span class="line">            sys.exit(<span class="number">0</span>)  <span class="comment"># 强制退出程序</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        server = ThreadingSocksServer((host, port), SocksProxy)</span><br><span class="line">        logger.info(<span class="string">f&quot;SOCKS5代理服务器启动在 <span class="subst">&#123;host&#125;</span>:<span class="subst">&#123;port&#125;</span>&quot;</span>)</span><br><span class="line">        server.serve_forever()</span><br><span class="line">    <span class="keyword">except</span> KeyboardInterrupt:</span><br><span class="line">        logger.info(<span class="string">&quot;收到KeyboardInterrupt信号&quot;</span>)</span><br><span class="line">        shutdown_server()</span><br><span class="line">    <span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line">        logger.error(<span class="string">f&quot;服务器运行时发生错误: <span class="subst">&#123;e&#125;</span>&quot;</span>)</span><br><span class="line">        shutdown_server()</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&#x27;__main__&#x27;</span>:</span><br><span class="line">    run_proxy()</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://stackli.me/posts/2025/net-proxy-demo</id>
    <link href="https://stackli.me/posts/2025/net-proxy-demo"/>
    <published>2025-05-19T16:04:38.000Z</published>
    <summary>
      <![CDATA[<p>在上一篇博客中，我们了解了几种常见的网络代理。本文将通过python代码来实现网络代理。代码仅做学习研究使用，不进行性能考虑。</p>
<h2 id="HTTP代理"><a href="#HTTP代理" class="headerlink"]]>
    </summary>
    <title>一文搞清楚常见网络代理-代码实战</title>
    <updated>2026-06-02T00:00:58.014Z</updated>
  </entry>
  <entry>
    <author>
      <name>stackli</name>
    </author>
    <category term="系统与网络" scheme="https://stackli.me/categories/%E7%B3%BB%E7%BB%9F%E4%B8%8E%E7%BD%91%E7%BB%9C/"/>
    <category term="网络代理" scheme="https://stackli.me/tags/%E7%BD%91%E7%BB%9C%E4%BB%A3%E7%90%86/"/>
    <content>
      <![CDATA[<p>在日常使用电脑的过程中，我们经常会遇到需要使用代理的情况。但每次在电脑上进行电脑配置时，都会很疑惑。这么多代理方式，我究竟应该选哪一种？</p><p>例如这个是MAC电脑上的代理配置。什么是网页代理，和安全网页代理有什么区别？什么是SOCKS代理？什么又是自动发现代理？<br><img src="/static/image/2022/mac-proxy-settings.png" alt="mac代理配置"></p><p>本文就将以MAC系统的代理配置为基础，同时鉴于我们常常使用的是HTTP&#x2F;HTTPS服务，来谈谈通过代理访问HTTP&#x2F;HTTPS服务的一些实现方式。</p><h2 id="HTTP代理"><a href="#HTTP代理" class="headerlink" title="HTTP代理"></a>HTTP代理</h2><p>HTTP代理即由一台HTTP服务器实现代理功能，包含两种实现方式：普通代理和隧道代理。</p><h3 id="普通代理"><a href="#普通代理" class="headerlink" title="普通代理"></a>普通代理</h3><p>HTTP普通代理在  XXXX 定义。以访问URL <a href="http://abc.xyz/index.html">http://abc.xyz/index.html</a> 为例</p><img  src=http://www.plantuml.com/plantuml/svg/UxfkqREExSzNhrVGjLFm20a9UB9xyVC9RS_cz3xjMlXqvWe5AmLtrn25Z9AI0Yjz_SIaPBsAoYhzpBoKr0gzZ9BS7B0UVKCzGovice5cFf-zxjc2TdJpi9g0hbGNfvDWMXIC30mK_BsnsmtK_HJkEmmz0000><ol><li>浏览器在识别到需要使用代理进行访问后，会向代理服务器发起请求，请求头为 GET <a href="http://abc.xyz/index.html">http://abc.xyz/index.html</a>  常规的http请求只会传递host之后的路径，由于代理服务器需要知道目标服务器的信息，所以这里请求路径为完整的目标url。 </li><li>代理服务器收到请求之后，会向正常的客户端一样，向目标服务器发起请求  GET  &#x2F;index.html</li><li>代理服务器在获取到目标服务器的响应之后，再将响应返回给客户端。</li></ol><p>在这个过程中，对于客户端来说，代理服务器扮演的是客户端的角色；对于目标服务器来说，代理服务器扮演的是客户端的角色。代理服务器在整个过程中，扮演“中间人”的角色。代理服务器需要能够识别HTTP请求的内容，因此这种形式的代理又可以认为是四层代理。</p><h3 id="隧道代理"><a href="#隧道代理" class="headerlink" title="隧道代理"></a>隧道代理</h3><p>上面谈到的普通代理，代理服务器需要识别HTTP请求的内容，才能对报文进行转发。但目前越来越多的网站采用的是HTTPS服务，HTTP报文都进行了端到端的加密，“中间人”方式的普通代理对加密数据就完全无能为力了。因此要对HTTPS服务进行代理，我们需要另外一种代理协议——隧道代理。<br>隧道代理的整个流程如下图：</p><img  src=http://www.plantuml.com/plantuml/svg/ixLLUDen--dkNIzdBN_PEFd5_ddF-fOgsDfoFLstw5d7zkUhrolesYbuX8G4FDcz-Fc4jcTpUfzsBNmwSuM2bOAplv-VgtE8Gc9Iibv5PPMLYOanMAM-ePuX5pOjG9EUprxtR44xiYaGwo4MSw5Bw-Aq7ABXwVPDGCtFLtTZjm-e6-uFa9OfJsVFWshSiF7vxbNFkfO-czPk1NUrWf61WO9pVbvUQd99PdwUGcfnIM9IJcPnHceA5vUjhSApgHkUzQvxicV1YtsJYGoaf_qJNpOqGZJYT2HcgBniw0TQ0zL6IgY0lFXqSk_JNLC81mdS7eW6Y6q0><p>浏览器先向代理服务器发送一个CONNECT请求, 让代理服务器创建一个到目标服务器的连接。 一旦连接创建好之后，浏览器就可以直接通过这个连接进行数据的发送，代理完全不用关心数据的内容，直接透传即可。理论上，隧道道理可以代理任何一种基于TCP的应用层协议，因此HTTP隧道代理又可称为三层代理。</p><p>如果代理的是HTTPS协议，那么连接建立以后，浏览器就可以像直接访问目标服务器一样，通过这个连接与目标服务器进行TLS的握手，以及握手成功之后的数据发送。使用HTTP这种不安全的协议代理HTTPS，能保证通信的安全吗？ 当然是可以的。我们可以看到在整个过程中，除了CONNEECT请求是明文的，其他的数据都要等到TLS握手成功后才会进行，而HTTPS本身就是中间人的天然克星，代理服务器无法获取密钥，也就无法对经过的数据进行解密。而对于CONNECT请求，除了域名和端口之外，不包括url、cookie等其他任何敏感信息。即使是CONNECT请求暴露的域名和端口，在正常不经代理的HTTPS请求中，也可以通过IP信息轻易得到。因此隧道代理并没有影响HTTPS协议的安全性。</p><p>正如前面所说，隧道代理可以代理任意一种基于TCP的应用层协议，因此也是可以代理HTTP协议的。然而在各个浏览器的实现都是：如果目标服务器是HTTP服务，那么浏览器会采用普通代理；如果目标服务器是HTTPS服务，则使用隧道代理。</p><h2 id="SOCKS代理"><a href="#SOCKS代理" class="headerlink" title="SOCKS代理"></a>SOCKS代理</h2><p>和HTTP代理协议是基于HTTP协议的一种扩展相比，SOCKS5协议则是一种直接为了代理而设计的一套协议。SOCK5协议和HTTP隧道代理有一些相似，都是先由客户端请求代理服务器创建到目标服务器的连接，连接创建完成之后，直接透传客户端与目标服务器之间的流量。但相比于HTTP的隧道代理，SOCKS5协议除了支持TCP协议之外，还支持UDP协议的透传。</p><img  src=http://www.plantuml.com/plantuml/svg/dP7BIiD058RtUOgpSnMpBxGRbovSz0bY6rHeBhR9ld4HYnAZf1Ka9QHQGIWaTbLiYdsPd9dqBHmp42zOI9tTNBxvvlzdxzGqZLvDaeoEh9cMsjblwQhKKyujHMifOljCFkdAAe00RDJhW8C0qsM-3idDSx40WcoPkcQfnqgNJJ1-98C5dIKWDg1vT73OpD8dUkkGiOjN4tnugK4UJ_F4HjV3aSE8lmw1oEZogXhmmnY4E7KPeXicOEBGo2RtRxZoTl5cwmcn4_JkDdsxefs3DFL9E6Q8ehMBFYevYwBoP47c5rjj3Q11JABBRzpFceLV-yK3jmc7dpNvBYw7LUCL5gQZi-iovDUFaz4hSUDi5UNlFl_m2hbvlnpT9VUcxp5JoVqlTwNKtCI1qWVm0G00><p>SOCKS协议的详细细节可以参考： <a href="https://www.mojidong.com/post/2015-03-07-socket5-1/">https://www.mojidong.com/post/2015-03-07-socket5-1/</a></p><h2 id="MAC中的配置"><a href="#MAC中的配置" class="headerlink" title="MAC中的配置"></a>MAC中的配置</h2><p>在回到前面说的MAC中的配置，经过验证之后发现：</p><ol><li>网页代理(HTTP)只有在访问HTTP服务时才会使用，且使用简单代理协议</li><li>安全网页代理(HTTPS)只有在访问HTTPS服务时才会使用，切使用隧道代理协议</li><li>如果配置了SOCKS代理，无论是否配置了网页代理和安全网页代理，都会优先采用SOCKS代理</li></ol>]]>
    </content>
    <id>https://stackli.me/posts/2022/net-proxy</id>
    <link href="https://stackli.me/posts/2022/net-proxy"/>
    <published>2022-03-20T09:53:51.000Z</published>
    <summary>
      <![CDATA[<p>在日常使用电脑的过程中，我们经常会遇到需要使用代理的情况。但每次在电脑上进行电脑配置时，都会很疑惑。这么多代理方式，我究竟应该选哪一种？</p>
<p>例如这个是MAC电脑上的代理配置。什么是网页代理，和安全网页代理有什么区别？什么是SOCKS代理？什么又是自动发现代理？<]]>
    </summary>
    <title>一文搞清楚常见网络代理-原理学习</title>
    <updated>2026-06-02T00:00:58.014Z</updated>
  </entry>
</feed>
