2026-01-19 00:00:00

最近重置了 Mac 系统,需要重新配置 Ruby 和 Jekyll 环境来运行我的静态博客。相比于直接使用系统 Ruby,这次选择了更优雅的 rbenv 进行版本管理。整个过程遇到不少“坑”,特此记录以备忘,希望这篇记录也能帮你绕开这些坑。
rbenv 将 Ruby 环境和所有 Gem 安装在你的用户目录下,实现完全的隔离管理,是当前 Ruby 社区推荐的最佳实践。
# 1. 通过 Homebrew 安装 rbenv 和 ruby-build(用来编译安装 Ruby)
brew install rbenv ruby-build
# 2. 初始化 rbenv,并按照提示把下面这行加到 ~/.zshrc 里
rbenv init
echo 'eval "$(rbenv init - zsh)"' >> ~/.zshrc
source ~/.zshrc # 让配置生效
# 3. 安装一个稳定的 Ruby 版本(这里选了 3.4.4,因为我系统显示最新的就是3.4.4)
rbenv install --list-all
rbenv install 3.4.4
rbenv global 3.4.4 # 设为默认版本
rbenv rehash # 重要:重建命令链接
# 4. 验证一下,确保 ruby 和 gem 命令指向的是 rbenv 安装的版本
which ruby # 应该显示 ~/.rbenv/shims/ruby
which gem # 应该显示 ~/.rbenv/shims/gem
ruby -v # 应该显示 3.4.4
# 5. 现在可以安全安装 Jekyll 了
gem install jekyll
本以为万事大吉,在博客目录下输入 jekyll s,结果错误连环出现。
直接报错,说项目 Gemfile 里锁定的 Jekyll 版本(4.3.2)和我刚全局安装的版本(4.4.1)对不上。

解决:在项目目录下,让 Bundler 根据 Gemfile 重新安装所有依赖。
bundle install

运行 bundle install 时,在编译 nokogiri 时卡住了,报错提示在找 x86_64 的库,但我的是 ARM 芯片(Apple Silicon)。
ld: warning: ignoring file '/opt/homebrew/Cellar/xz/5.8.1/lib/liblzma.dylib': found architecture 'arm64', required architecture 'x86_64'
原因:我的机器上好像混用了两个 Homebrew(Intel 和 ARM 版)。默认的 brew 命令指向了 Intel 版本。
解决:
brew 路径:which brew。我的是 /usr/local/bin/brew(Intel版)。eval $(/opt/homebrew/bin/brew shellenv)
bundle install,编译就通过了。public_suffix 版本激活冲突再次尝试 jekyll s,又出现新错误:全局的 public_suffix (7.0.2) 和项目需要的 (5.0.4) 冲突。
解决:改用 bundle exec 命令,严格使用项目 Gemfile.lock 里锁定的版本。
bundle exec jekyll s
以为终于行了,结果连续报错:
cannot load such file -- csvcannot load such file -- base64cannot load such file -- bigdecimal原因:Ruby 3.4 开始,像 csv、base64、bigdecimal、zlib、openssl 这些以前默认就有的标准库,现在需要单独作为 gem 安装了。而我的博客用的 Jekyll 版本比较旧,还依赖它们。
解决:在项目目录下,把这些缺失的库一次性补上。
bundle add csv base64 webrick bigdecimal zlib openssl
完成以上所有步骤后,再次运行:
bundle exec jekyll s
熟悉的启动信息终于出现了,浏览器打开 http://localhost:4000,博客本地预览恢复正常。
bundle exec 是护身符:在项目目录下运行任何 gem 相关的命令,都习惯性加上它,能避免很多奇怪的版本冲突。brew,能省去很多编译麻烦。bundle add 装啥,csv、base64、bigdecimal、zlib、openssl 这几个是常客。2026-01-17 00:00:00

最近在使用 Jekyll 开发博客时,遇到了一个典型问题:Gemfile 和 Gemfile.lock 文件 在本地和生产环境需要不同的配置。
git update-index --assume-unchanged
Git 提供了一个优雅的解决方案:assume-unchanged 标志。
assume-unchanged 是 Git 的一个内部标志,它告诉 Git:
“假设这个文件没有变化,不要检查它的修改状态,也不要让我提交它。”
# 从暂存区移除Gemfile文件
git restore --staged ../Gemfile ../Gemfile.lock
# 告诉 Git 忽略特定文件的本地修改
git update-index --assume-unchanged Gemfile
git update-index --assume-unchanged Gemfile.lock
# 查看哪些文件被标记为 assume-unchanged
git ls-files -v | grep '^h'
# 提交其他文件
git commit -m "更新博客文章,忽略Gemfile本地修改"
# 查看状态确认
git status
# 需要更新配置文件时
# 先恢复跟踪(撤销忽略)
git update-index --no-assume-unchanged Gemfile
git update-index --no-assume-unchanged Gemfile.lock
# 修改并提交
git add Gemfile
git commit -m "更新Gemfile依赖"
# 重新标记为忽略
git update-index --assume-unchanged Gemfile
git update-index --assume-unchanged Gemfile.lock
很多人会混淆 assume-unchanged 和 .gitignore,它们有本质区别:
| 特性 | assume-unchanged |
.gitignore |
|---|---|---|
| 用途 | 忽略已跟踪文件的修改 | 忽略未跟踪的文件 |
| 效果 | 文件仍在版本控制中,只是不检查修改 | 文件完全不被版本控制 |
| 适用场景 | 本地配置文件、环境变量文件 | 构建产物、日志文件、IDE配置 |
| 共享性 | 本地设置,不共享给他人 | 提交到仓库,团队成员共享 |
skip-worktree 的区别Git 还有一个类似的标志:skip-worktree。
# skip-worktree 的用法
git update-index --skip-worktree Gemfile
# 查看区别
git ls-files -v | grep '^S' # skip-worktree 文件
git ls-files -v | grep '^h' # assume-unchanged 文件
主要区别:
assume-unchanged:性能优化,告诉Git文件不太可能改变skip-worktree:功能标志,明确表示”不要更新我的工作树”对于配置文件管理,skip-worktree 是更安全的选择,因为它能防止 Git 的各种操作覆盖你的本地文件。
分支切换问题:
当你在标记了 assume-unchanged 的分支之间切换时,如果文件有冲突,Git 可能会报错。
# 假设团队成员更新了仓库中的 Gemfile
git pull
# 由于本地文件被标记,更新可能不会应用到你的工作副本
# 创建一个脚本帮助管理
cat > git-ignored-files.sh << 'EOF'
#!/bin/bash
echo "当前被忽略的文件:"
git ls-files -v | grep -E "^[hs]"
EOF
chmod +x git-ignored-files.sh
对于配置文件管理,还有更健壮的方案:
# 仓库中保存模板
_config.example.yml
Gemfile.example
# 本地复制并重命名
cp _config.example.yml _config.yml
cp Gemfile.example Gemfile
# _config.yml
url: <%= ENV['JEKYLL_SITE_URL'] || 'http://localhost:4000' %>
# 使用不同的配置文件
jekyll build --config _config.yml,_config.local.yml
git update-index --assume-unchanged 是一个强大的工具,特别适合管理那些需要在版本控制中保留,但又不想提交本地修改的文件。
最后,如果你经常忘记哪些文件被标记了,可以在 .gitconfig 中添加别名:
[alias]
ignored = !git ls-files -v | grep \"^[hs]\"
hide = update-index --assume-unchanged
unhide = update-index --no-assume-unchanged
然后使用更简洁的命令:
git ignored # 查看被忽略的文件
git hide Gemfile # 隐藏文件
git unhide Gemfile # 取消隐藏
2025-06-26 00:00:00

这篇文章简单记录自建DNS over HTTPS(DoH)服务的步骤。
100.100.100.100:53
satishweb/doh-server Docker容器(监听100.100.100.100:1053)# 停止并删除旧容器
docker stop doh && docker rm doh
# 启动DoH容器
docker run -d --restart unless-stopped \
--network host \
--name doh \
-e UPSTREAM_DNS_SERVER="udp:100.100.100.100:53" \
-e DOH_HTTP_PREFIX="/dns-query" \
-e DOH_SERVER_LISTEN="100.100.100.100:1053" \
-e DOH_SERVER_TIMEOUT="10" \
-e DOH_SERVER_TRIES="3" \
-e DOH_SERVER_VERBOSE="true" \
satishweb/doh-server
关键参数说明:
UPSTREAM_DNS_SERVER:上游DNS地址(需可被容器访问)DOH_SERVER_LISTEN:容器监听的本地地址和端口DOH_HTTP_PREFIX:DoH请求路径(必须与Nginx配置一致)在Nginx站点配置中(如/etc/nginx/sites-available/default)添加:
server {
listen 443 ssl;
server_name blog.kelu.org; # 替换为您的域名
# SSL证书配置(必需)
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
# DoH代理配置
location = /dns-query {
allow 100.100.100.5; # 允许访问的客户端IP
deny all; # 禁止其他IP
proxy_pass http://100.100.100.100:1053; # 指向DoH容器
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 其他配置...
}
操作命令:
sudo nginx -t && sudo systemctl reload nginx # 测试并重载配置
直接访问地址,验证是否成功:
https://aa.bb.com/dns-query?name=baidu.com&type=A

手工生成配置文件 doh.mobileconfig:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>DNSSettings</key>
<dict>
<key>DNSProtocol</key>
<string>HTTPS</string>
<key>ServerURL</key>
<string>https://blog.kelu.org/dns-query</string> <!-- 改为您的域名 -->
</dict>
<key>PayloadDescription</key>
<string>Configures DNS over HTTPS</string>
<key>PayloadDisplayName</key>
<string>DoH DNS Server</string>
<key>PayloadIdentifier</key>
<string>com.yourdomain.dns</string>
<key>PayloadType</key>
<string>com.apple.dnsSettings.managed</string>
<key>PayloadUUID</key>
<string>065AB183-5E34-4794-9BEB-B5327CF61F27</string> <!-- 用uuidgen生成 -->
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</array>
<key>PayloadDescription</key>
<string>Install to enable DNS over HTTPS</string>
<key>PayloadDisplayName</key>
<string>Custom DoH Configuration</string>
<key>PayloadIdentifier</key>
<string>com.yourdomain.dohprofile</string>
<key>PayloadUUID</key>
<string>030E6D6F-69A2-4515-9D77-99342CB9AE76</string> <!-- 用uuidgen生成 -->
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
ServerURL 为您的HTTPS地址
- 使用 uuidgen 命令生成新的 PayloadUUID
安装配置:
双击 .mobileconfig 文件导入macOS

提醒打开系统设定,在设备管理里可以看到:

双击后安装即可。
测试DoH服务:
curl -H 'content-type: application/dns-message' \
"https://blog.kelu.org/dns-query?dns=q80BAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB"
正常应返回加密的DNS响应。
客户端检查:
在macOS终端执行 scutil --dns | grep 'nameserver\[0\]'
前后对比,可以看到多了一个地址为 127.0.0.1 的 dns解析:

查看日志:docker logs doh
日志大概长这个样:

2025-06-25 00:00:00

wstunnel 是一个基于 WebSocket 的隧道工具,主要优势包括:
相关资料参考官方文档:https://github.com/erebe/wstunnel

# 创建专用目录
mkdir ~/wstunnel && cd ~/wstunnel
# 下载最新版
chmod +x wstunnel
/wstunnel server wss://[::]:1022 --restrict-to 127.0.0.1:22
监听 1022 端口,转发到22端口。
vi /etc/systemd/system/wstunnel.service
创建一个简单的服务:
# /etc/systemd/system/wstunnel.service
[Unit]
Description=wstunnel
After=network.target
[Service]
User=root
ExecStart=/root/wstunnel/wstunnel server wss://[::]:1022 --restrict-to 127.0.0.1:22
ExecReload=/bin/kill -SIGUSR1 $MAINPID
Restart=on-failure
[Install]
WantedBy=multi-user.target
# 重载 systemd
sudo systemctl daemon-reload
# 启动服务
sudo systemctl start wstunnel
# 设置开机自启
sudo systemctl enable wstunnel
# 检查状态
sudo systemctl status wstunnel

我是 M2 芯片,下载 wstunnel_10.4.3_darwin_arm64.tar.gz 解压到合适的位置,赋权:
chmod +x wstunnel
监听本地2222端口,ssh的目的IP和端口是127.0.0.1:22,后面是转发的服务器IP和1022端口
./wstunnel client -L tcp://2222:127.0.0.1:22 wss://<服务器IP>:1022
这里顺手记录一下如果是udp转发应该怎么写:
./wstunnel client -L "udp://12345:127.0.0.1:12345" wss://<服务器IP>:1234
ssh -p 2222 root@localhost
2025-06-24 00:00:00

Stunnel 是一个自由的跨平台软件,用于提供全局的TLS/SSL服务。针对本身无法进行TLS或SSL通信的客户端及服务器,Stunnel可提供安全的加密连接。
Stunnel可在许多操作系统下运行,包括Unix-like系统,以及Windows。
Stunnel 基于OpenSSL,要求已经安装了OpenSSL。Stunnel是开源的,支持所有SSL或TLS库所支持的。
在某些网络环境(如严格防火墙限制)中,直接SSH连接可能被阻断。通过 stunnel 建立 TLS 加密隧道:
brew install stunnel)apt install stunnel4)创建 ~/Workspace/bin/stunnel-ssh.conf:
# 全局配置
pid = /Users/kelu/run/stunnel.pid
foreground = yes
output = /Users/kelu/log/stunnel.log
debug = info
fips = no
# 客户端服务定义
[ssh-forward]
client = yes
accept = 127.0.0.1:2222 # 本地监听端口
connect = 你的服务器IP:22 # 替换为实际服务器IP
# SSL/TLS 配置
sslVersion = TLSv1.2
ciphers = AESGCM:ALL:!DH:!EXPORT:!RC4:+HIGH:!MEDIUM:!LOW:!aNULL:!eNULL
# 证书验证
verifyPeer = yes
CAfile = /etc/letsencrypt/live/blog.abc.com/fullchain.pem
checkHost = blog.abc.com # 证书域名验证
# 连接优化
socket = l:TCP_NODELAY=1
socket = r:TCP_NODELAY=1
TIMEOUTidle = 86400 # 超时设置
options = NO_TICKET
options = ALLOW_NO_DHE_KEX
启动隧道
stunnel ~/Workspace/bin/stunnel-ssh.conf

创建 /etc/stunnel/stunnel.conf:
# 全局配置
pid = /var/run/stunnel.pid
foreground = no
output = /var/log/stunnel4/stunnel.log
debug = info
fips = no
# SSH代理服务
[ssh-forward]
accept = 0.0.0.0:22 # 监听所有接口的22端口
connect = 127.0.0.1:22 # 转发到本地SSH
# protocol = proxy
# 证书配置
cert = /etc/letsencrypt/live/blog.abc.com/fullchain.pem
key = /etc/letsencrypt/live/blog.abc.com/privkey.pem
# 加密设置
sslVersion = TLSv1.2
ciphers = AESGCM:ALL:!DH:!EXPORT:!RC4:+HIGH:!MEDIUM:!LOW:!aNULL:!eNULL
sessionCacheSize = 1000
sessionCacheTimeout = 300
服务器配置中注释了
protocol = proxy,这是因为我这里使用了stunnel客户端直接连服务端,如果使用 proxy 会导致 Stunnel 在转发流量时添加额外的代理头(包含源地址和目的地址等信息),而 SSH协议无法处理这些头信息,从而导致连接失败。注意:PROXY协议通常用于需要传递客户端真实IP的场景(如HTTP反向代理),但SSH这类原生TCP协议不支持该扩展。移除后即可恢复正常通信。
重启服务:
sudo systemctl restart stunnel4

启动本地stunnel隧道
stunnel ~/Workspace/bin/stunnel-ssh.conf
通过代理连接SSH
ssh -p 2222 [email protected]
此时流量路径:
SSH客户端 → 本地2222端口 → TLS加密隧道 → 服务器22端口 → SSH守护进程
| 配置项 | 客户端作用 | 服务端作用 |
|---|---|---|
accept |
创建本地监听端口 | 指定服务监听端口 |
connect |
目标服务器地址 | 本地SSH服务地址 |
CAfile/cert |
验证服务器证书 | 提供有效TLS证书 |
checkHost |
验证证书域名匹配 | - |
TCP_NODELAY |
禁用Nagle算法降低延迟 | 同上 |
options |
优化TLS协议参数 | 优化TLS协议参数 |
TLS深度包检测(DPI) 系统,可以通过识别 Stunnel 的 TLS 指纹特征进行阻断。
2025-06-11 00:00:00

最近整了个WebShell容器方案。用Docker Compose跑起来倒是方便,但作为tmux重度用户,实际体验嘛,凑合能用。
直接上干活配置,存为docker-compose.yml就能用:
version: '3'
services:
webshell:
image: bwsw/webshell
container_name: webshell
network_mode: bridge
restart: "no"
environment:
- SSH_PORT=22 # 容器内SSH端口
- USERNAME=root # 默认登录用户
- DEFAULT_IP="100.100.100.100" # 记得改成公网IP
ports:
- "6666:80"
启动:
docker-compose up -d
访问 IP:6666 即可。
http://服务器IP:6666就能操作临时排查问题确实方便,但是对tmux用户的致命伤,tmux attach 进入后更容易遇到终端渲染抽风。