Featured image of post 從入門到進階:Tailscale + ShellCrash 異地組網和科學上網

從入門到進階:Tailscale + ShellCrash 異地組網和科學上網

這篇文章從 Tailscale 的原理講起,到自建 DERP 服務器優化連接質量,再到如何用 iptables 劫持 Tailscale Exit Node 的流量並轉發給 ShellCrash,實現流量的靈活轉發與安全穿透。

在幾次旅途中,我試圖將新拍的照片上傳到長沙家中的 Immich 服務器。過去我用 Cloudflare Tunnel 做反代,但由於國內特殊的網絡環境,連接慢速、頻繁中斷、上傳失敗幾乎成了常態。後來我開始嘗試 Tailscale —— 一個基於 WireGuard 的內網穿透工具,它終於讓我在全國各地都能穩定、高速地訪問家中的 NAS 和照片庫。

但新的問題也隨之而來:Tailscale 在 Android 手機上運行時,需要作為 VPN 服務接管系統流量,而我平時使用的 Clash 同樣依賴 VPN 接口進行分流。由於 Android 系統限制(只能啟用一個 VPN 服務),兩者無法共存,導致我必須在「訪問家中服務」和「科學上網」之間二選一。

這篇文章從 Tailscale 的原理講起,到自建 DERP 服務器優化連接質量,再到如何用 iptables 劫持 Tailscale Exit Node 的流量並轉發給 ShellCrash,實現流量的靈活轉發與安全穿透。無論你是想訪問家庭局域網上的 NAS、照片庫,還是希望在陌生網絡中保護自己的數據安全,這篇文章都能為你提供一套實用、穩定的解決方案。

什麼是 Tailscale

Tailscale 是一款基於 WireGuard 協議的零配置虛擬局域網工具,它能夠讓分佈在不同網絡環境中的設備像處於同一個安全內網中一樣互聯互通。通過自動穿透 NAT、防火牆等網絡障礙,Tailscale 讓你無需公網 IP、無需端口轉發,也能輕鬆訪問家中的 NAS、個人服務器、開發環境等內網資源。它的核心優勢在於簡單、安全、穩定,啟動即用,數據傳輸全程加密,適合個人開發者、遠程辦公者、家庭用戶等多種場景使用。

Tailscale 的技術實現非常巧妙:其構建在 WireGuard 加密協議之上,卻顛覆了傳統 VPN 的 IP 分配邏輯。每個設備通過 SSO/OAuth2 完成身份認證後,會獲得一個終身綁定的節點密鑰。這種基於身份的組網模式,讓「長沙的 NAS」和「香港的手機」在虛擬網絡中如同辦公室同事般直接對話。

Tailscale 的連接過程原理

中心控制服務器(Control Server)

每個 Tailscale 客戶端在啟動後,首先會連接控制服務器(controlplane),進行身份驗證,並拉取整個網絡中其他節點的信息,包括每臺設備的公網 IP、端口、NAT 類型等。這一步相當於是「認識朋友」。

Tailscale 的控制服務器不會轉發任何數據,只負責協調連接 —— 類似一個調度中心。

DERP 服務器

說到 Tailscale 能保持高連接成功率的關鍵,就不得不提到 Tailscale 自研的中轉協議 DERP,在 Tailscale 的網絡架構裡,DERP(Designated Encrypted Relay for Packets)是一個很重要但通常只在必要時介入的組件。簡單來說,它就是一個基於 HTTP 的加密中繼服務器,用來在兩臺設備無法直接通信時,作為它們之間的「中轉站」。

所有客戶端之間的連接都是先選擇 DERP 模式(中繼模式),這意味著連接立即就能建立,用戶無需等待。然後連接雙方開始並行地進行路徑發現,通常幾秒鐘之後,Tailscale 就能發現一條更優路徑,然後將現有連接透明升級(upgrade)過去,變成點對點連接(直連)。1

需要注意的是:

  • 所有通過 DERP 的數據都是端到端加密的,DERP 服務器無法查看內容;
  • Tailscale 會盡可能少地使用 DERP,一旦直連建立成功,就會自動切換過去;
  • 官方部署了多個分佈式 DERP 節點,客戶端會自動選擇延遲最低的那個;
  • 你也可以自建 DERP 節點(比如在國內),來解決延遲高或連接不穩定的問題。

可以把 DERP 理解為一個兜底機制,雖然性能不如直連,但確保了即便不能打洞成功,設備之間也始終能保持連接。

NAT 穿透(NAT Traversal)

拿到對端的地址信息後,Tailscale 會嘗試通過 NAT 穿透來建立點對點(P2P)連接。這個過程使用了 STUN 協議,雙方互相發送探測包,嘗試在 NAT 路由器上打出一條直連的通道。如果雙方的網絡條件允許,就可以成功建立起一個 UDP 的直連隧道,數據走直連,速度快、延遲低。

受制於篇幅,我無法完整細緻的講述 NAT 穿透的原理,若對這部分感興趣,可以閱讀 Tailscale 官方的「How NAT traversal works」一文。

完整連接流程圖示

自建 DERP

Tailscale 的安裝在各個平臺上都相對簡單,官方文檔已經提供了詳細的操作指南。本文將不再贅述安裝過程,以下內容默認你已經在相關設備上成功安裝並登錄了 Tailscale。

為什麼要自建 DERP?

Tailscale 在全球部署了眾多 DERP 2中繼服務器,用於在打洞失敗時接管流量中轉。但由於眾所周知的原因,中國大陸並沒有官方部署的 DERP 節點。這意味著:

  • 一旦 NAT 打洞失敗,所有流量都必須繞行海外的 DERP 節點,延遲高、速度慢,體驗極差;
  • 某些官方 DERP 節點容易被 GFW 干擾,可能出現連接中斷、握手失敗等問題;
  • 即使打洞成功,Tailscale 仍需通過 DERP 交換路由信息和 WireGuard 密鑰,如果 DERP 不可達,連接質量也會受到影響。

因此,在國內網絡環境下,自建一個本地 DERP 節點,不僅可以顯著提高連接穩定性和傳輸性能,還能規避部分網絡封鎖所帶來的不可預期問題,是一個非常值得做的優化。

準備工作

前面提到,DERP 是基於 HTTP 的,所以你需要準備好一個 HTTP 反代服務,並自行解決 SSL 證書的簽發等基礎問題。本文使用 Docker 部署,在部署開始之前,你需要在你的服務器上裝好 Docker 以及 Docker Compose 等附加組件。為了編輯配置文件,你當然也得知道如何使用 nano 之類的編輯器。為了最好的效果,你的服務器最好擁有靜態公網 IPv4+IPv6 雙棧地址。

如果以上條件都具備,就可以開始部署了。

1
2
mkdir tailscale-derp && cd tailscale-derp
nano docker-compose.yml

docker-compose.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
services:
  derper:
    name: tailscale-derp
    image: fredliang/derper
    environment:
      - DERP_DOMAIN=derp.nightcity.pub
      - DERP_VERIFY_CLIENTS=true
      - DERP_ADDR=:4433
    network_mode: host
    restart: unless-stopped
    volumes:
      - "/var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock"

network_mode: host是一個關鍵配置,表示容器將共享宿主機的網絡棧。如果使用 Docker 默認的 bridge 網絡模式,容器的網絡會經過 Docker 內網轉發,會造成 DERP 的 STUN 服務識別到 Docker 172.17.0.0/16網段下的地址,而 無法識別到客戶端正確的外網地址,導致 Tailscale 客戶端無法正確連接。

1
2
volumes:
  - "/var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock"

前面提到所有通過 DERP 的數據都是端到端加密的,DERP 並不知道是誰在使用,這意味著如果不採取措施,任何知道你 DERP 服務器地址和端口號的人都可以使用它。這條配置的作用是掛載宿主機的 Tailscale 套接字文件到容器內,目的是允許 derper 服務通過 Tailscale 的 tailscaled 服務進行身份驗證。配合DERP_VERIFY_CLIENTS=true,可以防止你的 DERP 節點被他人白嫖。

需要注意的是:

  • 宿主機必須已經安裝並登陸 Tailscale 客戶端(tailscaled),否則這個文件不存在,容器會報錯;
  • tailscaled 必須以 root 權限運行,才能創建這個 sock 文件。

反向代理

以 Caddy 為例,需要反向代理 4433 端口,併為其部署 SSL 證書。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
        email webmaster@l3zc.com
}

*.l3zc.com {
        encode gzip

        tls {
                dns dnspod APP_ID,APP_KEY
                resolvers 119.29.29.29 223.5.5.5
        }

        @derp host derp.l3zc.com
        reverse_proxy @derp :4433
}

需要注意的是,如果你和我一樣使用 Caddy 配合 dnsproviders 申請泛域名證書,Tailscale 的 MagicDNS 可能會導致 Caddy 本地證書驗證失敗而報錯,需要手動指定resolvers參數解決。

訪問剛剛反代的節點,如果出現以下頁面,說明配置正確。

DERP 搭建成功

配置 ACL 策略

打開 Tailscale 控制檯的「Access Controls」頁面配置 ACL 策略,將配置好的 DERP 加上。

Tailscale 的 ACL 策略是用 HuJSON3 寫的,想要在 VSCode 中編輯,選擇語言為「JSON with Comments(jsonc)」即可。以下是一個配置示例:

 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
{
  "acls": [{ "action": "accept", "src": ["*"], "dst": ["*:*"] }],
  "ssh": [
    {
      "action": "check",
      "src": ["autogroup:member"],
      "dst": ["autogroup:self"],
      "users": ["autogroup:nonroot", "root"]
    }
  ],

  // 自建 DERP 配置
  "derpMap": {
    "OmitDefaultRegions": false, // 改為 true 以排除官方 DERP
    "Regions": {
      "900": {
        "RegionID": 900,
        "RegionCode": "sha",
        "RegionName": "Shanghai",
        "Nodes": [
          {
            "Name": "myderp",
            "RegionID": 900,
            "HostName": "derp.l3zc.com"
          }
        ]
      }
    }
  }
}

Tailscale 保留RegionID中的 1-899 作為官方節點,自建節點的 RegionID 必須大於等於 900。

測試連接

配置好 ACL 並保存,Tailscale 會自動為所有客戶端同步配置,稍等片刻在客戶端用tailscale netcheck測試連接。

用 tailscale netcheck 測試連接

需要注意返回的 IP 是否是自己真實的公網 IP,若返回了172.17.0.0/16網段的地址,說明你 Docker 部分配置錯了。

與科學上網並存:在 Exit Node 劫持流量到 Clash 內核

聲明 Exit Node

在家中準備一個 24 小時開機的設備,可以是樹莓派,可以是 MacMini。在上面安裝 Tailscale 並將其聲明為 Exit Node,並根據需要在 Tailscale 內網聲明家庭內網網段,隨後在控制檯啟用這個設備作為 Exit Node,你就獲得了一個免費的 VPN,可以讓你在陌生的網絡環境中保持安全。

1
sudo tailscale up --advertise-exit-node --advertise-routes 192.168.1.0/24

找到 Route Settings 啟用相關設置

廣播完成後,無論身處何地,只要能連上 Tailscale 網絡,就能訪問家中所有的內網設備。

啟用 IP 轉發和禁用 UDP GRO4

啟用 IP 轉發是樹莓派等設備作為 Exit Node 所必須的配置,這裡以樹莓派為例,如果你使用其他設備,請自行查閱 Tailscale 官網教程。

1
2
3
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf

根據 Tailscale 官方的說法5,禁用 UDP GRO6 可以提升轉發性能,但官方的持久化教程似乎在樹莓派上無效,好在我們可以手動配置。

1
2
3
4
5
# 安裝 ethtool
sudo apt update && sudo apt install ethtool -y

# 關閉 UDP GRO
sudo ethtool -K eth0 gro off

針對持久化的問題手動編寫 systemd 配置文件:

1
2
# 創建服務文件
sudo nano /etc/systemd/system/ethtool.service

文件內容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[Unit]
Description=Configure eth0 GRO
After=network.target

[Service]
Type=oneshot
ExecStart=/sbin/ethtool -K eth0 gro off

[Install]
WantedBy=multi-user.target

隨後啟動服務:

1
2
3
sudo systemctl daemon-reload
sudo systemctl enable ethtool
sudo systemctl start ethtool

在 Exit Node 劫持流量

我的 Exit Node 是一臺樹莓派,在樹莓派上配置好 Exit Node 之後就來到了最後一步,也就是劫持手機發送到 Exit Node 上的流量,實現科學上網。出於穩定性原因,我不希望在家庭主路由上直接運行代理軟件,為了實現這一點,直接在樹莓派上劫持流量是唯一的選擇。

首先安裝 ShellCrash,請自行根據你的需要導入配置文件、配置自動任務等:

1
export url='https://fastly.jsdelivr.net/gh/juewuy/ShellCrash@master' && wget -q --no-check-certificate -O /tmp/install.sh $url/install.sh  && bash /tmp/install.sh && source /etc/profile &> /dev/null

隨後啟動服務,修改修改防火牆運行模式為純淨模式,我個人建議將 SNI 嗅探打開,並將 DNS 模式從fake-ip切換為redir-host7,同時啟用 IPv6 透明代理。

修改防火牆劫持範圍 啟用域名嗅探 修改端口設置

設置純淨模式的目的是手動配置 iptables 以實現更精準的流量劫持。我們直接用 iptables 劫持所有 tailscale 網卡作為 Exit Node 轉發的流量,首先用ifconfig查看 tailscale 網卡的名稱,默認情況下一般為 Tailscale 0:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
root@raspberrypi:~# ifconfig
eth0: ......

tailscale0: flags=4305<UP,POINTOPOINT,RUNNING,NOARP,MULTICAST>  mtu 1280
        inet 100.111.19.50  netmask 255.255.255.255  destination 100.111.19.50
        inet6 fd7a:115c:a1e0::3d01:1332  prefixlen 128  scopeid 0x0<global>
        inet6 fe80::c0d5:1a1b:2005:48eb  prefixlen 64  scopeid 0x20<link>
        unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00  txqueuelen 500  (UNSPEC)
        RX packets 6780155  bytes 958732442 (914.3 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 4811113  bytes 10858316472 (10.1 GiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

劫持tailscale0網卡的所有流量到本地 Clash 內核監聽端口7892,這個設置在 ShellCrash 中叫做「靜態路由端口」。以及,如果你不想和我一樣遇到莫名其妙的網絡問題,就一定不要忘記劫持 IPv6。

1
2
3
4
5
# IPv4 劫持 tailscale0
iptables -t nat -A PREROUTING -i tailscale0 -p tcp -j REDIRECT --to-ports 7892

# IPv6 劫持 tailscale0
ip6tables -t nat -A PREROUTING -i tailscale0 -p tcp -j REDIRECT --to-ports 7892

進階:用 TProxy 劫持 UDP 流量

iptables 的 REDIRECT 只能重定向 TCP 流量,UDP 沒有連接狀態(無連接協議),所以 REDIRECT 無法保留目標地址,導致透明代理無法知道原始目標地址。

所以,如果你用如下方式劫持 UDP 流量:

1
iptables -t nat -A PREROUTING -i tailscale0 -p udp -j REDIRECT --to-ports 7892

則這個規則不會生效或代理行為不正常。

那麼,有辦法代理 UDP 流量嗎?有的兄弟,有的。但前提是:

  • 核心支持 UDP 透明代理(Clash Premium 和 Mihomo 都支持);
  • 使用 TProxy 模式,而不是 REDIRECT;
  • 正確配置了 iptables mangle 表和 policy routing;
  • 代理配置文件中啟用了 UDP 代理(如 mode: rule + udp: true)。

假設你已經滿足第一條、第二條和最後一條,則以下是一個示例8

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 創建自定義鏈
sudo iptables -t mangle -N SHELLCRASH

# 根據自己的需要忽略本地流量
sudo iptables -t mangle -A SHELLCRASH -d 127.0.0.1/8 -j RETURN
sudo iptables -t mangle -A SHELLCRASH -d 100.64.0.0/10 -j RETURN
sudo iptables -t mangle -A SHELLCRASH -d 192.168.1.0/24 -j RETURN
sudo iptables -t mangle -A SHELLCRASH -d 172.17.0.0/16 -j RETURN

# mark UDP 和 TCP 到代理
sudo iptables -t mangle -A SHELLCRASH -p tcp -j TPROXY --on-port 7893 --tproxy-mark 233
sudo iptables -t mangle -A SHELLCRASH -p udp -j TPROXY --on-port 7893 --tproxy-mark 233

# 接口跳轉
sudo iptables -t mangle -A PREROUTING -i tailscale0 -j SHELLCRASH

# 路由表配置
echo "233 shellcrash" | sudo tee -a /etc/iproute2/rt_tables
sudo ip rule add fwmark 233 lookup shellcrash
sudo ip route add local 0.0.0.0/0 dev lo table shellcrash

當然,不要忘記 IPv6:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 創建鏈
sudo ip6tables -t mangle -N SHELLCRASH6

# 跳過本地地址
sudo ip6tables -t mangle -A SHELLCRASH6 -d ::1/128 -j RETURN
sudo ip6tables -t mangle -A SHELLCRASH6 -d fd7a:115c:a1e0::/48 -j RETURN

# 標記 TCP/UDP
ip6tables -t mangle -A SHELLCRASH6 -i tailscale0 -p tcp -j TPROXY --on-port 7893 --tproxy-mark 233
ip6tables -t mangle -A SHELLCRASH6 -i tailscale0 -p udp -j TPROXY --on-port 7893 --tproxy-mark 233

# 接口跳轉
sudo ip6tables -t mangle -A PREROUTING -i tailscale0 -j SHELLCRASH6

# 路由表配置
echo "233 shellcrash" | sudo tee -a /etc/iproute2/rt_tables
sudo ip -6 rule add fwmark 233 lookup shellcrash
sudo ip -6 route add local ::/0 dev lo table shellcrash

路由規則持久化

ip ruleip route創建的規則在重啟後會丟失,所以需要我們手動持久化,最簡單直接的方法就是創建一個腳本,並將其添加至 crontab。

創建一個腳本:

1
sudo nano /usr/local/bin/policy-route.sh

編輯為如下內容:

1
2
3
4
5
6
7
8
9
#!/bin/bash

# IPv4 策略路由
ip rule add fwmark 233 lookup 233
ip route add local 0.0.0.0/0 dev lo table 233

# IPv6 策略路由
ip -6 rule add fwmark 233 lookup 233
ip -6 route add local ::/0 dev lo table 233

授予執行權限後編輯 Crontab:

1
2
sudo chmod +x /usr/local/bin/policy-route.sh
sudo crontab -e

在 crontab 文件底部加上如下內容:

@reboot /usr/local/bin/policy-route.sh

原理解釋:TProxy 到底是怎麼轉發 UDP 流量的?

如果你看到這裡,也許會產生疑惑:為什麼整個流程中,我們沒有在 iptables 裡寫--to-ports,也沒看到目標地址被改寫,UDP 流量就莫名其妙地被代理了?這是怎麼做到的?

要解釋這個問題,我們先來看 TProxy 和 REDIRECT 的根本區別:

REDIRECT 模式:

  • 使用 iptables nat 表;
  • 將目標地址改寫為本地地址(比如 127.0.0.1:7892);
  • 通常用於 TCP 流量;
  • 不能保留真實目標地址;
  • 需要指定--to-ports

TPROXY 模式:

  • 使用 iptables mangle 表;
  • 不修改目標 IP,而是保留原始目標地址;
  • 通過 fwmark 和 policy routing 將報文路由到 lo
  • 代理程序監聽一個特殊端口(例如 7893),並啟用 IP_TRANSPARENT
  • 支持 UDP 和 TCP;
  • 不需要 iptables 內指定 --to-ports,因為不是 NAT,而是標記 + 路由。

TProxy 不使用 DNAT/REDIRECT,而是通過 mangle 表給數據包打上 mark,然後通過 policy routing(ip rule + ip route)將這些數據包送到 lo 接口。代理程序(如 Clash / ShellCrash)監聽在 lo9 上的端口(例如 7893),通過啟用 IP_TRANSPARENT 選項,可以讀取數據包的原始目標 IP 和端口並進行代理轉發。

簡而言之,TProxy 模式只需要:

  • iptables 給數據包打上 mark;
  • ip rule + ip route 將這些包送到 lo
  • 程序監聽 lo 上的端口並開啟 IP_TRANSPARENT

所以不需要在 iptables 中指定 --to-ports,因為目標 IP 和端口保持不變,代理程序自己可以感知並處理。

iptables 規則持久化

安裝iptables-persistent

1
2
sudo apt update
sudo apt install iptables-persistent

安裝過程中會提示你是否保存當前的 IPv4 和 IPv6 配置,選擇「是」即可。之後如果你添加了新的規則,記得執行保存命令:

1
2
# 保存當前 IPv4/IPv6 規則
sudo netfilter-persistent save

保存後的規則文件路徑:

  • IPv4:/etc/iptables/rules.v4
  • IPv6:/etc/iptables/rules.v6

你也可以直接編輯上面的rules.v4/rules.v6文件,按需修改。

最終效果

基本都能打洞成功 速度尚可接受

這套方案的使用體驗取決於你家的上行帶寬,我家的網絡是下行 500M 上行 60M,目前沒有遇到一次打洞失敗的情況,所以基本都能跑滿,延遲也尚可接受,並且可以在外地隨時隨地端到端加密訪問家中的 Immich 和 OpenWRT 路由器等設備以及實現科學上網,總體來看,我還算比較滿意。


  1. 參考: https://icloudnative.io/posts/custom-derp-servers/ ↩︎

  2. 參考: https://tailscale.com/kb/1232/derp-servers ↩︎

  3. 有關 HuJSON: https://github.com/tailscale/hujson ↩︎

  4. 參考: https://tailscale.com/kb/1408/quick-guide-exit-nodes ↩︎

  5. 參考: https://tailscale.com/kb/1320/performance-best-practices#linux-optimizations-for-subnet-routers-and-exit-nodes ↩︎

  6. UDP GRO(Generic Receive Offload)是 Linux 內核中的一種網絡優化技術,主要用於合併多個小數據包以提高處理效率。但在設備作為網絡轉發節點的使用場景下,這可能會導致轉發延遲增加和高丟包率環境下的吞吐量下降。 ↩︎

  7. 相較於fake-ipredir-host兼容性更好,出現問題的概率更低,也不會出現開關代理之後短時間內因為fake-ip殘留而斷網的情況,所以一般情況下我建議使用redir-host搭配 GeoSite 分流規則使用。我這臺樹莓派的 DNS 上游是已經配置好的 SmartDNS,不存在 DNS 汙染的問題,體驗良好。 ↩︎

  8. 參考: https://blog.zonowry.com/posts/clash_iptables_tproxy/ ↩︎

  9. lo 是 Linux 系統中默認的「迴環接口(Loopback Interface)」,在透明代理中,它不僅處理 localhost 流量,還被用來接收原本屬於外部世界的網絡連接,實現對外部流量的本地劫持和轉發。 ↩︎