家裡沒有 IPv6?我如何用 WireGuard + DualStack VPS Sing-Box 打通 IPv6

🥲 為什麼我需要這個架構?

我是自架服務玩家(VPS、BGP 也有自己的 ASN)。

我常常需要:

  • 測我自己的 IPv6 Anycast
  • 對外做 IPv6 API 服務測試
  • 測 DNS、DoQ、DoH 的 IPv6 traceflow
  • 測 Bird2 + RPKI 的 route propagation

但我家裡的網路: Kbro(IPv4 only)根本無法使用 IPv6 上游,也不能去測我自己廣播出的 IPv6 路由。

🎯 最終目標


Mac (只有 IPv4+Wireguard) → Kbro 中華電信 VPS + WireGuard VPN(IPv4) → Sing-Box → VPS(台灣 Dual-Stack) 讓我所有 IPv6 全部走 VLESS 出口

架構圖

Client (Mac)
fd42:42:44::2
⬇︎ (WireGuard IPv6)
wg0 on 中華電信 VPS 
⬇︎ (mangle → fwmark 0x1)
ip -6 rule fwmark 0x1 → table 100
⬇︎
table 100: default dev sb-tun0
⬇︎
sb-tun0 (sing-box tun inbound)
⬇︎ (VLESS over TLS)
DualStack VPS: 1.1.1.1 / 2a12:xxxx:123:xxx::
⬇︎
IPv6 Internet

Mac 上的 Wireguard 設定

[Interface]
PrivateKey = yEHMdZBaxxxxxwZ2U4=
Address = 10.8.3.2/24, fd42:42:44::2/128
DNS = 10.8.3.1
MTU = 1320

[Peer]
PublicKey = BB6wTT+ZMKxxxZI6vXUw8=
PresharedKey = d4tQpuxamExxxFN1bqGGM=
AllowedIPs = 0.0.0.0/0, ::/0, 10.8.3.0/24, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
Endpoint = xxx.com:1234
PersistentKeepalive = 15


流量路由流程(Network Flow)

  1. ip6tables (mangle) 做標記
ip6tables -t mangle -A PREROUTING -i wg0 -p tcp -j MARK --set-mark 0x1
ip6tables -t mangle -A PREROUTING -i wg0 -p udp -j MARK --set-mark 0x1

2. fwmark=0x1 -> 走 table 100

ip -6 rule add fwmark 0x1 table 100
ip -6 route add default dev sb-tun0 table 100

Note: IPv6 只要是從 wg0 進來,就會被送進 sb-tun0(Sing-Box)。

3. Sing-box (sb-tun0)

  • TUN Inbound 收到 IPv6 馮總
  • outbound=VLESS
  • 派送到 DualStack VPS IPv6 上游

4. DualStack VPS

  • IPv6/64 prefix

5. IPv6 回程 -> 原路回 Mac

Sing-box 安裝

https://github.com/SagerNet/sing-box/releases/download/v1.13.0-alpha.27/sing-box_1.13.0-alpha.27_linux_amd64.deb

dpkg -i sing-box_1.13.0-alpha.27_linux_amd64.deb

systemctl restart sing-box
systemctl status sing-box

iptables / routing Diagram(詳細流程)

(wg0 IPv6 traffic)
        │
        ▼
+-------------------+
| ip6tables mangle  |  mark: 0x1
+-------------------+
        │
 fwmark=0x1
        │
        ▼
+-----------------------+
| ip -6 rule table 100  |
+-----------------------+
        │
        ▼
+-----------------------+
| table 100 → default → sb-tun0 |
+-----------------------+
        │
        ▼
+----------------------+
|   Sing-Box tun-in    |
+----------------------+
        │
        ▼
+----------------------+
|  VLESS → DualStack VPS|
+----------------------+

Wireguard 設定 (中華電信)

[Interface]
Address = 10.8.3.1/24, fd42:42:44::1/128
# 允許 wg0 進來
PostUp = iptables  -A INPUT  -i %i -j ACCEPT
PostUp = ip6tables -A INPUT  -i %i -j ACCEPT

PostUp = iptables -A FORWARD -i %i   -o eth0 -j ACCEPT
PostUp = iptables -A FORWARD -i eth0 -o %i   -j ACCEPT

# IPv4 NAT:wg0 出 eth0 時做 MASQUERADE
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

# 允許 IPv6 轉發
PostUp = ip6tables -A FORWARD -i %i -j ACCEPT
PostUp = ip6tables -A FORWARD -o %i -j ACCEPT

# 還原規則
PostDown = iptables  -D INPUT  -i %i -j ACCEPT
PostDown = ip6tables -D INPUT  -i %i -j ACCEPT

PostDown = iptables -D FORWARD -i %i   -o eth0 -j ACCEPT
PostDown = iptables -D FORWARD -i eth0 -o %i   -j ACCEPT

PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

PostDown = ip6tables -D FORWARD -i %i -j ACCEPT
PostDown = ip6tables -D FORWARD -o %i -j ACCEPT


# IPv6:全部交給腳本處理 → fwmark + table 100 + sb-tun0
PostUp   = /root/wg0-ipv6.sh up %i
PostDown = /root/wg0-ipv6.sh down %i

中華電信 VPS 上 Sing-Box 設定(最小可用版)

{
  "inbounds": [
    {
      "type": "tun",
      "tag": "tun-in",
      "interface_name": "sb-tun0",
      "address": [
        "172.19.0.1/30",
        "fd00:19::1/126"
      ],
      "auto_route": false,
      "auto_redirect": false
    }
  ],

  "outbounds": [
    {
      "type": "vless",
      "tag": "to-xxx",
      "server": "1.1.1.1",
      "server_port": 4443,
      "uuid": "5f2c2f07-zzz-zz6-zzz-9fe5c6zzzzzz",
      "tls": {
        "enabled": true,
        "server_name": "xxxxxx"
      }
    },
    {
      "type": "direct",
      "tag": "direct-out"
    }
  ],

  "route": {
    "rules": [
      {
        "ip_version": 6,
        "outbound": "to-xxx"
      },
      {
        "ip_version": 4,
        "outbound": "direct-out"
      }
    ],
    "final": "direct-out"
  }
}



🧨 重點:IPv6 不能 NAT

  • 沒有 ip6tables -t nat -A POSTROUTING -j MASQUERADE
  • 因為 Debian 6.17 kernel 根本沒有 nat table
  • 而且 IPv6 NAT 本來就不該存在(會破壞端到端原則)
    只能 fwmark + policy routing → 強制走 sb-tun0

中華電信 VPS 上的 Script

#!/bin/bash
set -e

ACTION="$1"   # up / down
IFACE="$2"    # wg0

MARK=0x1
TABLE=100
TUN_IF="sb-tun0"

case "$ACTION" in
  up)
    # 開 IPv6 forward(只要開一次,重複設也沒差)
    sysctl -w net.ipv6.conf.all.forwarding=1 >/dev/null 2>&1 || true

    # mangle:標記從 wg0 進來的 IPv6 TCP/UDP
    ip6tables -t mangle -C PREROUTING -i "$IFACE" -p tcp -j MARK --set-mark $MARK 2>/dev/null || \
      ip6tables -t mangle -A PREROUTING -i "$IFACE" -p tcp -j MARK --set-mark $MARK

    ip6tables -t mangle -C PREROUTING -i "$IFACE" -p udp -j MARK --set-mark $MARK 2>/dev/null || \
      ip6tables -t mangle -A PREROUTING -i "$IFACE" -p udp -j MARK --set-mark $MARK

    # FORWARD:允許 wg0 ↔ sb-tun0
    ip6tables -C FORWARD -i "$IFACE" -o "$TUN_IF" -j ACCEPT 2>/dev/null || \
      ip6tables -A FORWARD -i "$IFACE" -o "$TUN_IF" -j ACCEPT

    ip6tables -C FORWARD -i "$TUN_IF" -o "$IFACE" -j ACCEPT 2>/dev/null || \
      ip6tables -A FORWARD -i "$TUN_IF" -o "$IFACE" -j ACCEPT

    # policy routing:fwmark=1 → table 100
    ip -6 rule | grep -q "fwmark $MARK lookup $TABLE" || \
      ip -6 rule add fwmark $MARK table $TABLE

    # table 100:default 走 sb-tun0
    ip -6 route replace default dev "$TUN_IF" table $TABLE
    ;;

  down)
    # 刪掉 mangle 規則
    ip6tables -t mangle -D PREROUTING -i "$IFACE" -p tcp -j MARK --set-mark $MARK 2>/dev/null || true
    ip6tables -t mangle -D PREROUTING -i "$IFACE" -p udp -j MARK --set-mark $MARK 2>/dev/null || true

    # 刪掉 FORWARD 規則
    ip6tables -D FORWARD -i "$IFACE" -o "$TUN_IF" -j ACCEPT 2>/dev/null || true
    ip6tables -D FORWARD -i "$TUN_IF" -o "$IFACE" -j ACCEPT 2>/dev/null || true

    # 清掉 policy routing & table 100
    ip -6 rule del fwmark $MARK table $TABLE 2>/dev/null || true
    ip -6 route flush table $TABLE 2>/dev/null || true
    ;;
  *)
    echo "Usage: $0 {up|down} <iface>" >&2
    exit 1
    ;;
esac

DualStack VPS 的 Singbox 設定

# /etc/sing-box/config.json
{
  "log": {
    "level": "info"
  },
  "inbounds": [
    {
      "type": "vless",
      "tag": "vless-in",
      "listen": "0.0.0.0",
      "listen_port": 4443,
      "users": [
        {
          "uuid": "5f2c2f07-xxx-xxx-xxx-9fe5c6xxxxxx",
          "flow": ""
        }
      ],
      "tls": {
        "enabled": true,
        "certificate_path": "/root/.acme.sh/xxxxxxx/fullchain.cer",
        "key_path": "/root/.acme.sh/xxxxxx/xxx.key"
      }
    }
  ],
  "outbounds": [
    {
      "type": "direct",
      "tag": "direct-out"
    }
  ]
}

純手動設定 + 測試

# 第一步
兩台 VPS 都安裝 Sing-box 和設定好相對應 /etc/sing-box/config.json

# 第二步
wg-quick up wg0

# 第三步
#(只清 IPv6 nat table,不動 filter / mangle 其他規則)
ip6tables -t nat -F
ip6tables -t nat -X

第四步 用 mangle 標記「從 wg0 來的 IPv6」

ip6tables -t mangle -F PREROUTING

# 從 wg0 進來的 IPv6 TCP/UDP,全部打上 fwmark = 1
ip6tables -t mangle -A PREROUTING -i wg0 -p tcp -j MARK --set-mark 0x1
ip6tables -t mangle -A PREROUTING -i wg0 -p udp -j MARK --set-mark 0x1

# 第五步 建立一個 routing table 100,所有 mark=1 的封包走這張表
# 新增一條規則:fwmark=1 → 用 table 100
ip -6 rule add fwmark 0x1 table 100
ip -6 rule del fwmark 0x1 table 100 2>/dev/null
ip -6 rule add fwmark 0x1 table 100
ip -6 route flush table 100
ip -6 route add default dev sb-tun0 table 100

第六步 在 table 100 裡面,預設路由改成丟到 sb-tun0
ip -6 route add default dev sb-tun0 table 100

#### 
這樣 
• 任何從 wg0 進來的 IPv6:
 • 被 mangle 標記 fwmark=1
 • 根據 ip -6 rule 被送去查 table 100
 • table 100 的 default route 是 dev sb-tun0
 • 封包原本 dst = 2606:4700:xxxx::xxxx 不變,但路由改成「交給 sb-tun0」
這時 sing-box 的 tun inbound 就能吃到完整 L3 封包。
### 

# 記得 interface 名稱要和 ip a 顯示完全一致:
ip a 

10: sb-tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 65535 qdisc fq_codel state UNKNOWN group default qlen 500
    link/none
    inet 172.19.0.1/30 brd 172.19.0.3 scope global sb-tun0
       valid_lft forever preferred_lft forever
    inet6 fd00:19::1/126 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::627e:7f30:b47c:5a3a/64 scope link stable-privacy proto kernel_ll
       valid_lft forever preferred_lft forever

# 檢查 table 100
ip -6 route show table 100

預期結果:
default dev sb-tun0 metric 1024 pref medium

第七步 允許 FORWARD(必要)
ip6tables -A FORWARD -i wg0 -o sb-tun0 -j ACCEPT
ip6tables -A FORWARD -i sb-tun0 -o wg0 -j ACCEPT

第八步 確認 IPv6 forwarding 開啟
grep -q "net.ipv6.conf.all.forwarding" /etc/sysctl.conf || \
  echo "net.ipv6.conf.all.forwarding=1" >> /etc/sysctl.conf
sysctl -p

測試

# 1. 確認 sing-box 正常

systemctl restart sing-box
systemctl status sing-box

看到:
•	inbound/tun[tun-in]: started at sb-tun0
•	sing-box started

# 中華電信 監聽
## sb-tun0 會跳一堆 IPv6 SYN 封包(你會看到)
tcpdump -ni sb-tun0 ip6

# 在 Mac 運行
curl -6 -k https://ifconfig.io
curl -6 -k https://ipapi.co/ip
curl -6 -k https://ipv6.ident.me

# 注意:為什麼「DNAT 到 fd00:19::1」一定會失敗?

DNAT 把目標 IP 改成本機 IP(fd00:19::1) → Linux 會當成本機服務 → 因為沒人聽 443 → RST → Connection refused。

TUN 要吃的是:
• 保持原目標 IP
• 但「路由」送去 TUN interface,而不是 DNAT 把 dst 改成本機。

Photo by Josiah Rock on Unsplash