# Derper 搭建

derper 是Tailscale/headscale 自建的中转节点，用来组网解决公共节点打洞成功率太低和延迟问题。
<!--more-->
# 自建 DERP 节点

## 序

之前用一台国外 VPS 配合 sing-box 开过 DERP。和公共 DERP 相比，带宽能跑满，但因为线路和 NAT 环境问题，UDP 打洞一直没成功。

后来换成国内云主机，改用官方 `derper` 二进制重新部署。

如果机器上没有现成的 Nginx 和证书体系，优先用更简单的官方直跑方案：

- `-a :443`
- `-http-port 80`
- `-certmode letsencrypt`

我最终采用的方案：`Nginx 监听 443`，后端 `derper` 监听 `12443`，证书由 Nginx 管理，`derper` 用 `manual` 模式读取证书。

---

## 最终架构

流量路径如下：

```text
客户端 -> TCP 443 -> Nginx -> https://127.0.0.1:12443 -> derper
客户端 -> UDP 3478 ---------------------------------------> derper(STUN)
```

对应关系：
- `443/tcp`：DERP 客户端入口，走 Nginx
- `3478/udp`：STUN 打洞，直达 `derper`
- `12443/tcp`：后端 `derper` 监听端口，只给本机 Nginx 用，阿里云防火墙策略里面不对公网放行

`--verify-clients` 用来防止其他人白嫖
如果开启 `--verify-clients`：
- DERP 机必须安装并接入 `tailscaled`
- `derper` 会通过本机 `tailscaled` 判断客户端是不是自己 tailnet 的成员

---

## 端口规划

至少要确认这些端口：

- `443/tcp`：放行
- `3478/udp`：放行
- `80/tcp`：只在你需要 Nginx 做 ACME 或 HTTP 跳转时放行
- `12443/tcp`：不要在云安全组里放行

这样外部只能通过 Nginx 进来，但 STUN 还能正常工作。

---

## 前提条件

假设：
- 系统是 `Debian 12`
- 域名是 `derp.example.com`
- 域名已经解析到 VPS 公网 IP
- 证书由 Nginx 或其他现有流程维护

如果准备开启 `--verify-clients`，还需要：
- 安装 `tailscaled`
- 让这台 DERP 机加入正确的 tailnet
- 最好给它打上 `tag:derp`

如果机器上启用了 Tailscale，且 `apt update` 超时，优先确认不是 DNS 或私网镜像源问题。长期建议：


```bash
sudo tailscale set --accept-dns=false --netfilter-mode=off
```

---

## 安装 Go 和 `derper`

验证 Go：
```bash
go version
```

如果没装 Go，自行安装到 `/usr/local/go` 后，补 PATH：
```bash
echo 'export PATH=$PATH:/usr/local/go/bin' | sudo tee /etc/profile.d/go.sh
```

创建运行目录：
```bash
sudo useradd --system --home /var/lib/derper --create-home --shell /usr/sbin/nologin derp
sudo install -d -o derp -g derp -m 0750 /var/lib/derper /var/lib/derper/certs
```

安装 `derper`：
```bash
sudo env PATH=$PATH GOBIN=/usr/local/bin /usr/local/go/bin/go install tailscale.com/cmd/derper@latest
```

如果国内网络太慢：
```bash
sudo env PATH=$PATH GOBIN=/usr/local/bin GOPROXY=https://goproxy.cn,direct /usr/local/go/bin/go install tailscale.com/cmd/derper@latest
```

验证：
```bash
/usr/local/bin/derper -version
```

---

## 安装并接入 Tailscale

如果要用 `--verify-clients`，DERP 机必须先入网：
```bash
curl -fsSL https://tailscale.com/install.sh | sh
sudo systemctl enable --now tailscaled
sudo tailscale up
sudo tailscale set --advertise-tags=tag:derp
```

检查状态：
```bash
tailscale version
systemctl status tailscaled
sudo tailscale status
```

---

## 证书准备

如果你已经有 Nginx 管理的证书，例如：
- `/etc/nginx/ssl/derp/cert.pem`
- `/etc/nginx/ssl/derp/key.pem`

那么 `derper` 使用 `-certmode manual` 时，不会直接认 `cert.pem` 和 `key.pem` 这种名字。它要求证书文件名必须和主机名一致：

- `derp.example.com.crt`
- `derp.example.com.key`

因此可以用软链接映射：

```bash
sudo mkdir -p /var/lib/derper/certs
sudo ln -sf /etc/nginx/ssl/derp/cert.pem /var/lib/derper/certs/derp.example.com.crt
sudo ln -sf /etc/nginx/ssl/derp/key.pem /var/lib/derper/certs/derp.example.com.key
```

这是这次排障里最关键的坑之一：

- 开了 `-certmode manual`
- 却没配 `-certdir`
- 或者证书文件名不匹配主机名

结果就是 `derper` 直接启动失败，Nginx 对后端只能拿到 `502 Bad Gateway`。

---

## `derper.service`

最终采用的 `systemd` 配置如下：

`/etc/systemd/system/derper.service`

```ini
[Unit]
Description=Tailscale DERP Server
Wants=network-online.target tailscaled.service
After=network-online.target tailscaled.service
Requires=tailscaled.service

[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/var/lib/derper
NoNewPrivileges=true

ExecStart=/usr/local/bin/derper \
  -c /var/lib/derper/derper.key \
  -hostname derp.example.com \
  -a :12443 \
  -http-port -1 \
  -stun-port 3478 \
  -certmode manual \
  -certdir /var/lib/derper/certs \
  -stun \
  -verify-clients

Restart=on-failure
RestartSec=3

[Install]
WantedBy=multi-user.target
```

这里有几个关键点：

- `User=root`：开启 `--verify-clients` 时，这样最省事，避免本地 `tailscaled.sock` 权限问题
- `-a :12443`：不要写成 `127.0.0.1:12443`
- `-http-port -1`：关闭 `derper` 自己的 HTTP 监听
- `-stun-port 3478`：STUN 端口是纯数字，不要写成 `:3478`
- `-certmode manual` + `-certdir`：让 `derper` 使用你已经准备好的证书
- `-verify-clients`：限制只允许自己 tailnet 的客户端使用

启动：
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now derper
sudo systemctl status derper
sudo journalctl -u derper -f
```

---

## Nginx 反代配置

示例：
`/etc/nginx/sites-available/derp`

```nginx
server {
    listen 443 ssl http2;
    server_name derp.example.com;

    ssl_certificate /etc/nginx/ssl/derp/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/derp/key.pem;

    location / {
        proxy_pass https://127.0.0.1:12443;
        proxy_ssl_verify off;
        proxy_ssl_server_name on;
        proxy_ssl_name derp.example.com;

        proxy_buffering off;
        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;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_connect_timeout 60s;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }
}
```

启用并检查：
```bash
sudo ln -sf /etc/nginx/sites-available/derp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```

这次排障确认了几个容易错的点：
- `proxy_pass` 必须是 `https://127.0.0.1:12443`，不能写成 `http://`
- `proxy_ssl_server_name on` 和 `proxy_ssl_name derp.example.com` 最好一起带上
- `proxy_buffering off` 建议开启，DERP 这种长连接/流式转发场景更稳

---

## tailnet policy 和 `derpMap`

示例：
```json
{
  "tagOwners": {
    "tag:derp": ["autogroup:admin"]
  },
  "derpMap": {
    "omitDefaultRegions": false,
    "regions": {
      "901": {
        "RegionID": 901,
        "RegionCode": "sz",
        "RegionName": "cn-sz DERP",
        "Nodes": [
          {
            "Name": "901a",
            "RegionID": 901,
            "HostName": "derp.example.com",
            "IPv4": "1.2.3.4",
            "CanPort80": true,
            "DERPPort": 443,
            "STUNPort": 3478
          }
        ]
      }
    }
  },
  "grants": [
    {
      "src": ["tag:derp"],
      "dst": ["*"],
      "ip": ["*"]
    }
  ]
}
```

这里的重点是：
- `DERPPort` 写 `443`，因为客户端实际连的是 Nginx，不是后端 `12443`
- `STUNPort` 还是 `3478`
- 开启 `--verify-clients` 时，不要把 ACL 写得过死，至少先保证 `tag:derp` 能“看见”允许使用它的节点

ACL 写太严时，`tailscale ping` 和节点探测都出现异常。先跑通，再慢慢收紧规则更稳妥。

如果需要 `ping`，别忘了 ACL 里要考虑 `icmp`。

---

## 验证方法

### 服务状态

```bash
sudo systemctl status derper
sudo journalctl -u derper -n 100 --no-pager
```

### 端口监听

```bash
sudo ss -tlnp | grep 12443
sudo ss -ulnp | grep 3478
```

期望结果：

- `12443/tcp` 在监听
- `3478/udp` 在公网地址上监听，而不是只绑在 `127.0.0.1`

### 域名访问

```bash
curl -I https://derp.example.com
```

### Tailscale 状态

```bash
tailscale netcheck
tailscale ping <peer>
tailscale status
```

如何判断：

- 看到自定义 DERP 区域，说明 `derpMap` 生效
- 看到 `relay "sz"` 之类信息，说明 DERP 可作为中转保底
- 看到 `direct`，说明 UDP 打洞成功，已经不是纯 DERP 中转

---

## 这次排障里最关键的结论

 1. `502 Bad Gateway` 不等于真的是 443 被拦了
很多时候只是 Nginx 能起来，但后端 `derper` 没起来。
先查：
```bash
sudo systemctl status derper
sudo journalctl -u derper -n 50 --no-pager
sudo ss -tlnp | grep derper
```

如果 `12443` 没监听，优先看 `derper` 启动参数和证书。

 2. `manual` 模式下，证书目录和文件名必须对
这次最开始的 `502`，根因之一就是：
- `derper` 开了 `-certmode manual`
- 但没找到正确的证书
要么缺 `-certdir`，要么证书不是按主机名命名。

 2. Nginx 反代 `derper` 时，上游必须走 HTTPS
`derper` 在这套配置下是 TLS 服务。如果 Nginx 写成：
```nginx
proxy_pass http://127.0.0.1:12443;
```
那就属于协议对不上，最后可能得到 `400` 或 `502`。

 4. `-a 127.0.0.1:12443` 会把 STUN 一起绑到回环地址
这是这次踩的另一个大坑。
如果把 `-a` 绑到 `127.0.0.1`，很多版本的 `derper` 会让 `3478/udp` 也只监听回环地址。结果就是：
- 网页可能正常
- DERP 中转可能正常
- 但 `direct` 很难成功，因为外部客户端根本打不到 STUN
因此最终保留：
```bash
-a :12443
-stun-port 3478
```
然后通过云安全组不开放 `12443/tcp` 来补安全。

 5. `derpMap` 要指向 443，不是 12443
客户端连的是：
```text
客户端 -> 443 -> Nginx -> 12443 -> derper
```
所以 `DERPPort` 必须写 `443`。

 6. `--verify-clients` 依赖本机 `tailscaled` 的可见范围
如果日志里出现：
```text
client nodekey:... rejected: peer nodekey:... not authorized (not found in local tailscaled)
```
优先检查：
- DERP 机是否已经加入正确的 tailnet
- `tag:derp` 是否配置正确
- ACL / grants 是否让 DERP 机能看到这些客户端
如果只是临时排障，也可以先去掉 `-verify-clients`，等网络和反代都跑通再加回来。

 7. 海外节点访问异常时，要分清是配置问题还是安全组/线路问题
如果外地或海外节点完全打不到你的自建 DERP：
先在远端做两类测试：
```bash
curl -I https://derp.example.com
nc -zvu 1.2.3.4 3478
```

然后回头检查：
- 云安全组是否放行了 `443/tcp`
- 云安全组是否放行了 `3478/udp`
- `12443/tcp` 是否没有对公网放行
- 是否确实存在跨境线路抖动或区域性阻断

---

## 安全建议

这一套最终的安全边界是：

- 公网开放 `443/tcp`
- 公网开放 `3478/udp`
- 公网不开放 `12443/tcp`
- `derper` 开启 `--verify-clients`

这样即使 `derper` 进程监听了 `:12443`，外部也很难直接打到它。

如果你只是临时测试：

- 可以先不启用 `--verify-clients`
- 先把 `Nginx -> derper`、`derpMap`、`STUN` 跑通
- 最后再加访问控制

---

## 最终判断标准

满足以下条件就算部署完成：

- `derper.service` 是 `active (running)`
- `curl -I https://derp.example.com` 正常返回
- `3478/udp` 对外可达
- `tailscale netcheck` 能看到自定义 DERP 区域
- 需要中转时能走到自建 DERP
- 网络允许时能出现 `direct`
- 未授权客户端会被 `--verify-clients` 拒绝

---

## 参考
### 官方文档
1.  [DERP servers](https://tailscale.com/docs/reference/derp-servers)
2. [Device connectivity](https://tailscale.com/kb/1411/device-connectivity)
3. [Group devices with tags](https://tailscale.com/kb/1068/tags)
4. [Grants syntax](https://tailscale.com/docs/reference/syntax/grants)
5. [Tailscale CLI reference](https://tailscale.com/docs/reference/tailscale-cli)

### 官方仓库
- [cmd/derper/README.md](https://github.com/tailscale/tailscale/tree/main/cmd/derper)
- [cmd/derper/derper.go](https://github.com/tailscale/tailscale/blob/main/cmd/derper/derper.go)
- [cmd/derper/cert.go](https://github.com/tailscale/tailscale/blob/main/cmd/derper/cert.go)



---

> 作者: Chifan  
> URL: https://eo.chifan.de/posts/derper/  

