Skip to main content

tailscale 自建服务器

为什么是VPN?

前面讲过,我企图打通各个住所和学校的内网。列位要问了,你不是搞过FRP内网穿透吗,为啥还要VPN?我个人的理解是:FRP侧重于服务,依托于开放的端口;VPN侧重于互连,依托于C/S架构和IP,对比于下表。可见,要打通各个内网,必须使用基于VPN的技术才行。

对比项 FRP VPN
开放端口数 随服务数增加 很少
主要应用 对外提供服务,网页服务较多 对内提供连通
穿透方向 单向 双向(通过路由)
安全性 一般
IP级互连 不支持 支持
额外的客户端 不需要 一般需要
部署难度 容易 困难

用哪个VPN?

关于主流VPN技术,下面这篇文章总结的挺好。

anonymous:WireGuard到底好在哪? 

我斗胆再一句话总结下:PPTP不安全;OpenVPN针对IPSec/L2TP做了减法;WireGuard针对OpenVPN又做了减法,性能更高,还支持了去中心化。

可见,WireGuard是目前最先进的VPN技术,已被引入Linux内核,必须选她!

还有个原因,群晖的VPN服务端都被阉割了,自己装套件起OpenVPN也不行;威联通的OpenVPN服务端可以,但静态路由设置时总是出错。

为什么是Headscale

WireGuard目前只是一个内核级别的模块,想要配置好裸的WireGuard,低代码是别想了,那么多对端秘钥,增、删节点都需要改动所有节点的配置,想一想就头疼!

表扬威联通,已经支持图形化界面的WireGuard服务器和客户端。

基于WireGuard的上层应用,目前比较成熟的有TailscaleNetmakerTailscale 是在用户态实现了 WireGuard 协议;Netmaker 直接使用了内核态的WireGuard,理论上性能更高,但目前缺乏中继机制(类似FRP),应用场景受限。HeadscaleTailscale的开源实现,适合私有部署,就选她了!

本文动机

知乎上介绍Headscale的很少;找遍全网,也很少有低代码、快速部署Headscale的文章,能讲清楚原理和为什么这样配置的就更少了。

仍然要感谢一些博主,虽然不讲原理,但内容确实丰富,给我一定启发(其实是偷懒不用去看文档了),比如下面这个。

Tailscale玩法之内网穿透、异地组网、全隧道模式、纯IP的双栈DERP搭建、Headscale协调服务器搭建,用一期搞定,看一看不亏吧?

我在群晖和威联通的NAS上都用docker-compose部署成功了,必须向大家汇报下,希望能帮助更多非专业领域的“私有云折腾师”。

Headscale搭建

架构介绍

主节点(我自己定义的概念)的网络拓扑如下图所示。其他节点与之类似,不包含服务端及其UI。

主节点网络拓扑

服务端(server),又叫协调服务器。负责WireGuard节点的公钥交换、虚拟IP分配、路由转发的公开和访问控制。

客户端(client),即WireGuard节点。目前仍然使用的是Tailscale的开源客户端,采用go语言编写,在用户空间实现WireGuard

中继端(derp),是P2P连接时NAT穿透的保底方案。DERP(Detoured Encrypted Routing Protocol)是Tailscale自研的协议,运行在 HTTP 之上 ,根据目的公钥来中继加密的流量。中继端同时支持DERP和STUN。

关于NAT穿透的原理,可以参考下面这篇。

NAT穿透arthurchiao.art/blog/how-nat-traversal-works-zh/

可见,服务端负责控制,中继端负责数据通路,客户端发起/接受连接,是可以部署在不同的服务器上的。这里我们资源有限,把他们都部署在一个NAS里,还需要使用反向代理(lucky以“零代码”支持带SSL证书的HTTPS访问;为了“低代码”配置服务端,我们给她再加一个服务端控制界面(webui,以下简称UI端),齐活。

关于客户端,其实有两个作用。一是做为WireGuard节点连到大内网里。

这时,为了减少路由的层级,其容器的网络类型一般设为host。

二是通过Unix的进程间通信(sock)为中继端提供用户认证,防止中继端被他人使用。

通过把客户端和中继端的/var/run/headscale链接在一起来实现。这时,其容器的网络类型最好设为bridge。

如何选择容器网络类型,可以参考下面的公式。

假设,中继端部署在服务器A上,负责VPN路由的是服务器B。
if(A == B)
  在A上部署客户端,容器网络使用host。
else {
  在A上部署客户端,容器网络使用bridge或host都行。
  在B上部署客户端;如果使用容器,其网络使用host。// 例如,OpenWRT上可以直接部署。
}

关于自定义的容器子网,可以参考下面这篇文章。

IC民工:NAS添加静态路由失效的主要原因:容器子网滥用 

我把这些容器都部署在一个NAS上,所以用host。相关的端口如下表,使用了基于子域名的lucky反向代理后,只需要对公网(别忘了在路由器上做端口映射)暴露一个STUN的UDP3478端口(新增)和一个lucky反向代理的端口(例如8080,已有)。相比FRP,美极了。

服务端 UI端 中继端DERP 中继端STUN
端口类型 TCP TCP TCP UDP
容器侧端口 8080 7070 6060 3478
NAS侧端口 58080 57070 56060 3478
HTTPS反向代理 需要 需要 需要 不需要

容器编排

直接给出带注释的四合一docker-compose.yaml,全网罕见。

version: '3.9'

networks: # 定义编排容器的子网
  private:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.200.0/24

services:
  server: # 服务端
    image: headscale/headscale
    container_name: headscale-server
    networks:
      - private
    volumes:
      - ./headscale/config:/etc/headscale # 提前放好config.yaml和derp.yaml
      - ./headscale/data:/var/lib/headscale
      - ./headscale/run:/var/run/headscale
      - /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro # 使用NAS的时间
    ports:
      - "58080:8080" # listen port
    command: serve # v0.22及以前的版本需要使用headscale serve
    restart: unless-stopped
    depends_on:
      - derp

  webui: # UI端
    image: ghcr.io/gurucomputing/headscale-ui
    container_name: headscale-ui
    networks:
      - private
    environment:
      HTTP_PORT: 7070
    ports:
      - "57070:7070" 
    volumes:
      - /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
    restart: unless-stopped

  derp: # 中继端
    image: fredliang/derper
    container_name: headscale-derp
    networks:
      - private
    environment:
      DERP_DOMAIN: derp.bakeding.site # 替换为自己的域名
      DERP_ADDR: :6060 # 注意,前面有个英文冒号
      DERP_CERT_MODE: letsencrypt # 使用了lucky做反向代理,理论上不需要设置,但我还没试过。
      DERP_VERIFY_CLIENTS: true # 还用client做认证时,配置为true
    ports:
      - "56060:6060" # derp port, TCP
      - "3478:3478/udp"  # STUN port, UDP
    volumes:
      - ./tailscale:/var/run/tailscale
      - /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
    restart: unless-stopped
    depends_on:
      - client

  client: # 客户端
    image: tailscale/tailscale
    container_name: headscale-client
    network_mode: "host" # 用做连接各子网的客户端时,这样最简单
    privileged: true
    environment:
      TS_EXTRA_ARGS: --netfilter-mode = off # 默认不开启路由转发,更灵活
    volumes:
      - ./tailscale:/var/run/tailscale # 要在NAS上和derp共享同一个目录
      - /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
      - /var/lib:/var/lib
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - net_admin
      - sys_module
    command: tailscaled
    restart: unless-stopped

注意,要提前配置好config.yamlderp.yaml。可以去GitHUB的代码仓,下载config-example.yamlderp-example.yaml,修改好内容(见下文)并重命名。

headscale代码仓

我用的是latest映像,当前对应源码的版本是v0.23.0-alpha5。配置文件如果报错,可以去搜一下Issues,一般都有答案。

另外,只需要把docker-compose.yamlserverwebui的部分注释掉,就可以部署在其他节点。如果不想增加中继端,也可以把derp的部分注释掉。

服务端配置

config.yaml中修改的地方如下。

  • server_url要改成反向代理后的网址。
  • urls下面的网址注释掉,不使用官方的中继端。
  • 增加derp.yaml的位置,指定自己搭建的中继端。
  • 注意各端口要和docker-compose.yaml中的对应。
    server_url: https://tailscale.bakeding.site
    listen_addr: 0.0.0.0:8080
    # Address to listen to /metrics, you may want to keep this endpoint private to your internal network
    metrics_listen_addr: 0.0.0.0:9090
    grpc_listen_addr: 0.0.0.0:50443 # 看起来没啥用
    ip_prefixes:
      100.100.0.0/16
      # List of externally available DERP maps encoded in JSON
      urls:
        #- https://controlplane.tailscale.com/derpmap/default
    
      # Locally available DERP map files encoded in YAML
      paths:
        - /etc/headscale/derp.yaml

derp.yaml如下,这里我添加了两个中继端。

# If you plan to somehow use headscale, please deploy your own DERP infra: https://tailscale.com/kb/1118/custom-derp-servers/
regions:
  901:
    regionid: 901
    regioncode: huoyan
    regionname: tencent
    nodes:
      - name: tencent-derp
        regionid: 901
        hostname: derp.bakeding.site
        stunport: 3478
        stunonly: false
        derpport: 56060

      902:
        regionid: 902
        regioncode: hk
        regionname: Hongkong Telecom
        nodes:        
          - name: shelter2-derp
            regionid: 902
            hostname: derp.mirror.example.com
            stunport: 3478
            stunonly: false
            derpport: 56060

反向代理配置

headscale-server和derp正常进行反代就行了,ui端因为跨域的原因,需要在同一个域里。如您按照本文提供的内容进行创建的话,那么你可以直接使用此nginx配置,否则请自行修改相应端口。

server {
	#监听443端口
    listen 443 ssl;
    #你的域名
    server_name tailscale.bakeding.site; 
	access_log /var/log/nginx/tailscale443.log;
    #ssl证书的pem文件路径
    ssl_certificate  /www/cert/tailscale.bakeding.site_bundle.pem;
    #ssl证书的key文件路径
    ssl_certificate_key /www/cert/tailscale.bakeding.site.key;
	
    location ^~ / {
		proxy_pass http://127.0.0.1:58080; 
		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 REMOTE-HOST $remote_addr; 
		proxy_set_header Upgrade $http_upgrade; 
		proxy_set_header Connection "upgrade"; 
		proxy_set_header X-Forwarded-Proto $scheme; 
		proxy_http_version 1.1; 
		add_header X-Cache $upstream_cache_status; 
		add_header Strict-Transport-Security "max-age=31536000"; 
		add_header Cache-Control no-cache; 
	}

	location ^~ /web {
		proxy_pass http://127.0.0.1:57070; 
		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 REMOTE-HOST $remote_addr; 
		proxy_set_header Upgrade $http_upgrade; 
		proxy_set_header Connection "upgrade"; 
		proxy_set_header X-Forwarded-Proto $scheme; 
		proxy_http_version 1.1; 
		add_header X-Cache $upstream_cache_status; 
		add_header Strict-Transport-Security "max-age=31536000"; 
		add_header Cache-Control no-cache; 
	}

}
server {
	listen       80;	#监听80端口
	listen  [::]:80;
	server_name  tailscale.bakeding.site;		#也可以填写自己注册的域名
	access_log /var/log/nginx/default.log;

	location ^~ / {
		proxy_pass http://127.0.0.1:56060; 
		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 REMOTE-HOST $remote_addr; 
		proxy_set_header Upgrade $http_upgrade; 
		proxy_set_header Connection "upgrade"; 
		proxy_set_header X-Forwarded-Proto $scheme; 
		proxy_http_version 1.1; 
		add_header X-Cache $upstream_cache_status; 
		add_header Strict-Transport-Security "max-age=31536000"; 
	}

	location ^~ /web {
		proxy_pass http://127.0.0.1:57070; 
		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 REMOTE-HOST $remote_addr; 
		proxy_set_header Upgrade $http_upgrade; 
		proxy_set_header Connection "upgrade"; 
		proxy_set_header X-Forwarded-Proto $scheme; 
		proxy_http_version 1.1; 
		add_header X-Cache $upstream_cache_status; 
		add_header Strict-Transport-Security "max-age=31536000"; 
		add_header Cache-Control no-cache; 
	}

  
    #error_page  404              /404.html;
    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;		#错误页面设置
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

server {
	#监听443端口
    listen 443 ssl;
    #你的域名
    server_name derp.bakeding.site; 
	access_log /var/log/nginx/derp443.log;
    #ssl证书的pem文件路径
    ssl_certificate  /www/cert/derp.bakeding.site_bundle.pem;
    #ssl证书的key文件路径
    ssl_certificate_key /www/cert/derp.bakeding.site.key;
	
    location ^~ / {
		proxy_pass http://127.0.0.1:56060; 
		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 REMOTE-HOST $remote_addr; 
		proxy_set_header Upgrade $http_upgrade; 
		proxy_set_header Connection "upgrade"; 
		proxy_set_header X-Forwarded-Proto $scheme; 
		proxy_http_version 1.1; 
		add_header X-Cache $upstream_cache_status; 
		add_header Strict-Transport-Security "max-age=31536000"; 
	}

}
server {
	listen       80;	#监听80端口
	listen  [::]:80;
	server_name  tailscale.bakeding.site;		#也可以填写自己注册的域名
	access_log /var/log/nginx/default.log;

	location ^~ / {
		proxy_pass http://127.0.0.1:56060; 
		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 REMOTE-HOST $remote_addr; 
		proxy_set_header Upgrade $http_upgrade; 
		proxy_set_header Connection "upgrade"; 
		proxy_set_header X-Forwarded-Proto $scheme; 
		proxy_http_version 1.1; 
		add_header X-Cache $upstream_cache_status; 
		add_header Strict-Transport-Security "max-age=31536000"; 
	}

  
    #error_page  404              /404.html;
    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;		#错误页面设置
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

服务端操作(步骤1)

其实服务端支持很多命令行操作,但我们追求“低代码”,只需要用命令行生成一个API Key,剩下的工作在UI端点鼠标就行了。

进入容器,执行命令,把生成的API Key记录下来:ynREpBS.UduOZ_Dwxu1WRreh6vMKvUsiNwvefz_n

$ headscale apikeys create -e 9999d

其中,-e后面指定的是过期时间,这里我指定9999天,27年后看能否有人攻破。

也可以在宿主机上执行,前面加sudo docker exec -it即可,不会的可以练练。

UI端操作(步骤2)

  1. 打开UI的URL,本例为https://tailscale.bakeding.site/web

2. 进入“Settings”。

3. 添加“Headscale URL”,本例为https://tailscale.bakeding.site

4. 把服务端生成的Key添加到“Headscale API Key”。

5. 点击“Test Server Settings”,出现绿色对号后UI端就可以接管服务端了,如下图所示。

UI端添加API Key

6. 进入“User View”,点击“+New User”,添加一个用户。

UI端添加用户

7. 为该用户生成一个Preauth Key,供客户端连接使用。为了便捷性,最好设置为“Reusable”,并“Active”,如下图。

UI端添加Preauth Key

连接的密钥设置比较灵活,有两种方法。一种是上面这种:在服务端生成Preauth Key(1个共享或多个独立),客户端连接时指定,成功后在“Device View”里就能看到各个节点。另一种是在客户端连接时生成,在UI端的“Device View”里手动添加秘钥、注册节点。我这么懒惰,当然共享1个Preauth Key。

客户端操作(步骤3)

  1. 进入各客户端的容器,执行命令。
   tailscale up --netfilter-mode=off \
                 --accept-routes \
                 --advertise-routes=192.168.2.0/24 \
                 --login-server=https://tailscale.bakeding.site \
                 --auth-key=c5765d7426fe55c005ffda74419ec38f9a32fd770fa13199
  • --accept-routes代表接受其他节点的路由指示。
  • --advertise-routes指定本节点对其他节点的路由建议,即哪个网段走VPN到本节点。一般是本节点的内网网段。
  • --login-server指定服务端的URL。
  • --auth-key指定在UI端生成的Preauth Key。

2. 打开UI端网页,进入“Device View”,把各节点的“Device Routes”设置为“active”,如下图。

这里还可以看到各个节点分配的VPN IP地址。

UI端开启Devic Routes

NAS配置(步骤4)

要在NAS上开启路由转发,把VPN路由过来的包转发到内网。

  1. 通过ssh登录到NAS,执行命令。

    $ ip addr

2. 找到NAS的内网IP地址所对应的虚拟网卡名,我这里是ovs_eth0;找到VPN地址所对应的网卡名,我这里是tailscale0

3. 执行命令:启用IPv4转发功能;防火墙配置了两个网络接口(ovs_eth0tailscale0)的数据包转发规则,并执行网络地址转换(NAT)操作。使能了VPN子网和内网的双向互访。

   sudo iptables -I FORWARD -i ovs_eth0 -j ACCEPT
   sudo iptables -I FORWARD -o ovs_eth0 -j ACCEPT
   sudo iptables -t nat -I POSTROUTING -o ovs_eth0 -j MASQUERADE
   sudo iptables -I FORWARD -i tailscale0 -j ACCEPT
   sudo iptables -I FORWARD -o tailscale0 -j ACCEPT
   sudo iptables -t nat -I POSTROUTING -o tailscale0 -j MASQUERADE
   sudo sysctl -w net.ipv4.ip_forward=1

4. 最后,把它们加到群晖的“计划任务”,开机触发启动。

  • 去掉所有sudo,以root执行。
  • 为了保证VPN相关的容器先启动,最上面最好加个sleep 1m

主路由配置(步骤5)

为了让本节点内网的其他地址也能通过VPN访问其他节点的内网,需要在主路由上添加静态路由,例如下表。

描述 目的地址 子网掩码 下一跳地址 出接口
访问VPN节点 100.100.0.0 255.255.0.0 本节点NAS地址 LAN
访问其他节点的内网 其他节点的内网网段 其他节点的内网掩码 本节点NAS地址 LAN

经过ping测试,大功告成!

参考文章 Tailscale/Headscale自建异地组网 Tailscale+Headscale+自建Derp踩坑记录 已验证:自建Tailscale的 DERP 中继教程