tailscale比较好用,而且免费用户福利也没有什么大幅度缩水,由于国内网络和国外不互通,所以官方的DERP服务并不好用,有必要自建一个国内的私有DERP给自己用,几年前我就建过一个,最近整理服务器迁移事宜,所以写了一个更详尽的文档记录一下
官方文档写的并不是很详细,但是还是有一定的参考价值,https://tailscale.com/kb/1118/custom-derp-servers ,我的思路是给STUN和HTTPS都设置了不知名的特殊端口,手动管理了TLS证书,所以并不需要开放80端口用于自动化申请证书
同时,我采用了URL验证客户端的方案,防止有人白嫖….
整体docker-compose.yml文件如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| services: derper: image: fredliang/derper:208878d628c7c6cad604da7798b6deee3894c7a6 container_name: derper environment: - DERP_DOMAIN=xx.domain.com - DERP_CERT_DIR=/etc/letsencrypt/live/domain.com - DERP_CERT_MODE=manual - DERP_ADDR=:port - DERP_STUN=true - DERP_STUN_PORT=stun_port - DERP_HTTP_PORT=-1 - DERP_VERIFY_CLIENT_URL=http://verify_client:3000 ports: - "port:port" - "stun_port:stun_port/udp" volumes: - /etc/letsencrypt/live/domain.com:/etc/letsencrypt/live/domain.com:ro restart: unless-stopped
verify_client: build: context: . dockerfile: Dockerfile container_name: verify_client volumes: - ./nodes.json:/app/nodes.json:ro restart: unless-stopped
|
此处使用了Github项目,免得自己构建docker镜像,此处TLS的证书,我是使用了certbot申请了泛域名证书,得到的文件名并不能直接被derper使用,因此有一个小脚本cp到了适合的文件名
update_cert_and_restart_docker.sh
1 2 3 4 5 6 7 8
| #!/bin/bash
cp /etc/letsencrypt/live/domain.com/fullchain.pem /etc/letsencrypt/live/domain.com/xx.domain.com.crt cp /etc/letsencrypt/live/domain.com/privkey.pem /etc/letsencrypt/live/domain.com/xx.domain.com.key
/usr/bin/docker compose -f /root/derper/docker-compose.yml up --build --force-recreate -d
|
另外加入了cron定时任务,让它每半个月重启一次,以使用新证书
1
| 0 2 1,15 * * /root/derper/update_cert_and_restart_docker.sh
|
防白嫖配置
放在公网上,derp还是有比较明显的特征的,因此我决定加一个鉴权,本来打算基于tailscale的API写一个的,但是它的API最多90天,这样限制就比较大,需要经常更新,我个人的节点很固定,所以我决定写死,有新设备加入手动修改
获得当前网络中所有的节点,写入nodes.json文件
1
| tailscale status --json | jq '[recurse | objects | with_entries(select(.key == "PublicKey")) | .[]] | sort'
|
写了一个最简的鉴权脚本verify_client.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| package main
import ( "encoding/json" "log" "net/http" "os" )
type DERPAdmitClientRequest struct { NodePublic string Source string }
type DERPAdmitClientResponse struct { Allow bool }
var allowedNodes []string
func loadNodes(filename string) error { data, err := os.ReadFile(filename) if err != nil { return err }
var nodeKeys []string if err := json.Unmarshal(data, &nodeKeys); err != nil { return err }
for _, nodeStr := range nodeKeys { allowedNodes = append(allowedNodes, nodeStr) }
return nil }
func handleAdmitRequest(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return }
var req DERPAdmitClientRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.WriteHeader(http.StatusBadRequest) return }
allowed := false for _, node := range allowedNodes { if node == req.NodePublic { allowed = true break } }
resp := DERPAdmitClientResponse{Allow: allowed} if err := json.NewEncoder(w).Encode(resp); err != nil { w.WriteHeader(http.StatusInternalServerError) return }
if allowed { log.Printf("Node admitted: %x", req.NodePublic) } }
func main() { if err := loadNodes("nodes.json"); err != nil { log.Fatal("Failed to load nodes:", err) }
http.HandleFunc("/", handleAdmitRequest)
log.Println("Server listening on :3000") if err := http.ListenAndServe(":3000", nil); err != nil { log.Fatal(err) } }
|
构建脚本
Dockerfile
1 2 3 4 5 6 7 8 9
| FROM golang:alpine3.20 AS builder WORKDIR /app COPY verify_client.go ./ RUN go build -o verify_client verify_client.go
FROM alpine:latest WORKDIR /app COPY --from=builder /app/verify_client /app/ CMD ["./verify_client"]
|
至此,部署完毕,在Tailscale 后台设置一下,我个人是禁用了默认的DERP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| "derpMap": { "OmitDefaultRegions": true, "Regions": { "900": { "RegionID": 900, "RegionCode": "my", "Nodes": [{ "Name": "1", "RegionID": 900, "HostName": "xx.domain.com", "DERPPort": port, "STUNPort": stun_port, }], }, }, },
|
另,此种方案还需要有域名,互联网上有人分享了不用域名,仅使用ip配合自签名证书的方案,不过我没有尝试
参考
如何利用 Caddy 搭建 Tailscale 的 Custom DERP Servers
tailscale-derp-client-verifier