自建 DNS over HTTPS(DoH)并在 macOS 配置使用

这篇文章简单记录自建DNS over HTTPS(DoH)服务的步骤。

架构说明

  1. 上游DNS100.100.100.100:53
  2. DoH服务satishweb/doh-server Docker容器(监听100.100.100.100:1053
  3. 前端代理:Nginx提供HTTPS并反向代理到DoH服务
  4. 客户端:macOS通过配置文件接入

一、部署DoH服务器(Docker容器)

# 停止并删除旧容器
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反向代理

在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

image-20250626午後51343065

三、客户端配置(macOS)

  1. 手工生成配置文件 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
  2. 安装配置

    • 双击 .mobileconfig 文件导入macOS

      image-20250626午後51135751

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

      image-20250626午後51214021

      双击后安装即可。

四、验证服务

  1. 测试DoH服务

    curl -H 'content-type: application/dns-message' \
      "https://blog.kelu.org/dns-query?dns=q80BAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB" 
    

    正常应返回加密的DNS响应。

  2. 客户端检查

    • 在macOS终端执行 scutil --dns | grep 'nameserver\[0\]'
    • 查看日志:docker logs doh

    日志大概长这个样:

    image-20250626午後55524139


使用 wstunnel 在 macOS 上安全连接 Linux 服务器

背景

wstunnel 是一个基于 WebSocket 的隧道工具,主要优势包括:

  • 将任意 TCP/UDP 流量封装在 WebSocket 中
  • 支持 TLS 加密(wss://)
  • 轻量级且跨平台(Rust 编写)
  • 能有效绕过深度包检测(DPI)、

服务端配置

相关资料参考官方文档:https://github.com/erebe/wstunnel

1. 下载 wstunnel

image-20250626午後43714110

# 创建专用目录
mkdir ~/wstunnel && cd ~/wstunnel

# 下载最新版
chmod +x wstunnel

2. 简单运行测试

/wstunnel server wss://[::]:1022 --restrict-to 127.0.0.1:22

监听 1022 端口,转发到22端口。

3. 创建 systemd 服务

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

3. 启动并启用服务

# 重载 systemd
sudo systemctl daemon-reload

# 启动服务
sudo systemctl start wstunnel

# 设置开机自启
sudo systemctl enable wstunnel

# 检查状态
sudo systemctl status wstunnel

客户端配置(macOS)

1. 安装 wstunnel

image-20250626午後43714110

我是 M2 芯片,下载 wstunnel_10.4.3_darwin_arm64.tar.gz 解压到合适的位置,赋权:

chmod +x wstunnel

2. 本地客户端

监听本地2222端口,ssh的目的IP和端口是127.0.0.1:22,后面是转发的服务器IP和1022端口

./wstunnel client -L tcp://2222:127.0.0.1:22 wss://<服务器IP>:1022

3. 连接

ssh -p 2222 root@localhost

使用 Stunnel 安全代理 SSH 连接(macOS客户端 + Debian服务器)

Stunnel 是一个自由的跨平台软件,用于提供全局的TLS/SSL服务。针对本身无法进行TLS或SSL通信的客户端及服务器,Stunnel可提供安全的加密连接。

Stunnel可在许多操作系统下运行,包括Unix-like系统,以及Windows。

Stunnel 基于OpenSSL,要求已经安装了OpenSSL。Stunnel是开源的,支持所有SSL或TLS库所支持的。

为什么需要SSH代理?

在某些网络环境(如严格防火墙限制)中,直接SSH连接可能被阻断。通过 stunnel 建立 TLS 加密隧道:

  1. 将SSH流量伪装成HTTPS流量
  2. 添加额外的TLS加密层
  3. 绕过端口限制(使用标准443端口)

环境准备

  • 客户端: macOS (安装stunnel: brew install stunnel)
  • 服务器: Debian (安装: apt install stunnel4)
  • 有效域名证书 (我使用Let’s Encrypt)

客户端配置 (macOS)

创建 ~/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

image-20250624午後34622363

服务端配置 (Debian)

创建 /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

image-20250624午後34857765

连接方式

  1. 启动本地stunnel隧道

    stunnel ~/Workspace/bin/stunnel-ssh.conf
    
  2. 通过代理连接SSH

    ssh -p 2222 username@127.0.0.1
    

此时流量路径: SSH客户端 → 本地2222端口 → TLS加密隧道 → 服务器22端口 → SSH守护进程

关键配置说明

配置项 客户端作用 服务端作用
accept 创建本地监听端口 指定服务监听端口
connect 目标服务器地址 本地SSH服务地址
CAfile/cert 验证服务器证书 提供有效TLS证书
checkHost 验证证书域名匹配 -
TCP_NODELAY 禁用Nagle算法降低延迟 同上
options 优化TLS协议参数 优化TLS协议参数

TLS指纹识别

TLS深度包检测(DPI) 系统,可以通过识别 Stunnel 的 TLS 指纹特征进行阻断。


用 Docker Compose 跑 WebShell:临时救急还行,但tmux党慎入

最近整了个WebShell容器方案。用Docker Compose跑起来倒是方便,但作为tmux重度用户,实际体验嘛,凑合能用。

我的docker-compose配置

直接上干活配置,存为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 即可。

优点:开箱即用

  1. 三秒部署 - 一条命令直接拉起带Web界面的SSH环境
  2. 隔离性好 - 所有操作封在容器里,玩坏了删容器重建
  3. 多协议支持 - 浏览器直接访问http://服务器IP:6666就能操作

临时排查问题确实方便,但是对tmux用户的致命伤,tmux attach 进入后更容易遇到终端渲染抽风


Shell脚本中的八进制陷阱:解决日期前导零问题

最近在执行一个用于计算月度时间统计的脚本时,遇到了一个有趣的错误。这个脚本本应计算当月已经过去的小时数,但在每月的8日和9日却会神秘地失败。错误信息十分具有误导性,让人一时难以找到问题所在:

./tmp.sh: line 20: 08: value too great for base (error token is "08")
(standard_in) 1: syntax error
(standard_in) 1: syntax error

问题代码

出错的脚本片段如下:

timecheck=$(date "+%Y-%m-%d_%H:%M:%S")

# 固定使用30天720小时计算
month_days=30
total_hours=720

# 获取当前日期、小时和分钟
current_day=$(date +%d)
current_hour=$(date +%H)
current_minute=$(date +%M)

# 计算已过去的小时数(当前日的小时 + 已过去的天数*24)
hours_passed=$(( (current_day - 1) * 24 + current_hour ))

错误发生在第20行,也就是计算hours_passed的那一行。

问题原因

这个问题看似简单的计算为何会出错?原因在于Shell中数字解析的一个隐藏陷阱:在Bash等Shell环境中,以0开头的数字默认会被解析为八进制(base-8)数字。

在八进制系统中,只允许使用0-7这八个数字。而在每月的8日和9日,date +%d命令返回的是”08”和”09”,这在八进制中是非法的,因此导致了”value too great for base”的错误。

这是一个典型的Shell编程陷阱,特别容易在处理日期和时间时遇到,因为日期和时间格式通常会包含前导零。

解决方案

针对这个问题,有两种有效的解决方案:

方案1:使用无前导零的日期格式

# 获取不带前导零的日期和时间
current_day=$(date +%-d)
current_hour=$(date +%-H)
current_minute=$(date +%-M)

通过在格式说明符前添加-符号(如%-d而不是%d),可以让date命令返回没有前导零的数字,从而避免八进制解析的问题。

方案2:强制使用十进制解析

如果你的系统不支持无前导零的日期格式(例如某些macOS版本),可以使用以下方法强制以十进制解析:

# 明确指定使用十进制解析
current_day=$((10#$(date +%d)))
current_hour=$((10#$(date +%H)))
current_minute=$((10#$(date +%M)))

通过添加10#前缀,我们明确告诉Shell使用十进制(base-10)来解析这些数字,无论它们是否有前导零。


macOS 选择特定 Profile 命令行启动 Chrome

最近在尝试用 Selenium 复用本地 Chrome 浏览器的登录状态时,遇到了 Profile 路径的问题。记录一下 macOS 下快速定位 Chrome 用户 Profile 的方法。

什么是 Chrome 的 Profile?

Chrome 的 Profile 是浏览器用户数据的隔离单元,每个 Profile 独立保存书签、密码、扩展程序等信息。默认情况下,Chrome 会创建名为 Default 的主 Profile,你也可以手动添加多个 Profile(如 Profile 1Profile 2)。

在自动化测试中,通过 --user-data-dir 参数加载已有 Profile,可保留 Cookie 和密码自动填充功能。

查看当前 Profile 的路径

  1. 打开 Chrome 地址栏,输入 chrome://version 并回车。
  2. 在页面中找到 “Profile Path” 字段,显示的路径类似:

     /Users/你的用户名/Library/Application Support/Google/Chrome/Default
    

    这里的 Default 文件夹即为当前用户的 Profile 数据目录 。在父目录下,会看到多个文件夹,如 DefaultProfile 1 等,每个文件夹对应一个 Profile 。

快速启动

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --user-data-dir=/Users/kelu/Library/Application\ Support/Google/Chrome --profile-directory=Profile\ 1