Initial commit
This commit is contained in:
+887
@@ -0,0 +1,887 @@
|
||||
# MeshRay 关键技术详解
|
||||
|
||||
## 目录
|
||||
|
||||
1. [雪花算法 ID 生成](#1-雪花算法 id 生成)
|
||||
2. [Conn.Bind 模型与 9 层传输](#2-connbind-模型与 9 层传输)
|
||||
3. [Mesh 中继架构](#3-mesh-中继架构)
|
||||
4. [ExternalService 服务市场](#4-externalservice-服务市场)
|
||||
5. [MeshSeed 安全机制](#5-meshseed-安全机制)
|
||||
|
||||
---
|
||||
|
||||
## 1. 雪花算法 ID 生成
|
||||
|
||||
### 算法结构
|
||||
|
||||
```
|
||||
64 位整数:
|
||||
┌─┬──────────────────────┬────────────┬─────────────┐
|
||||
│0│ 41 bits 时间戳 │ 10 bits │ 12 bits │
|
||||
│ │ (毫秒级,69 年) │ 节点 ID │ 序列号 │
|
||||
└─┴──────────────────────┴────────────┴─────────────┘
|
||||
↑ ↑ ↑
|
||||
符号位 机器标识 单毫秒内自增
|
||||
```
|
||||
|
||||
### Go 实现
|
||||
|
||||
```go
|
||||
package snowflake
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Snowflake struct {
|
||||
mu sync.Mutex
|
||||
lastStamp int64 // 上次生成 ID 的时间戳
|
||||
sequence int64 // 当前毫秒内的序列号
|
||||
machineID int64 // 机器 ID(0-1023)
|
||||
}
|
||||
|
||||
func NewSnowflake(machineID int64) *Snowflake {
|
||||
return &Snowflake{
|
||||
machineID: machineID,
|
||||
lastStamp: -1,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Snowflake) Generate() int64 {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
stamp := time.Now().UnixMilli()
|
||||
if stamp < s.lastStamp {
|
||||
panic("时钟回拨")
|
||||
}
|
||||
|
||||
if stamp == s.lastStamp {
|
||||
// 同一毫秒内,序列号自增
|
||||
s.sequence = (s.sequence + 1) & 0xFFF
|
||||
if s.sequence == 0 {
|
||||
// 序列号溢出,等待下一毫秒
|
||||
for stamp <= s.lastStamp {
|
||||
stamp = time.Now().UnixMilli()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 新毫秒,重置序列号
|
||||
s.sequence = 0
|
||||
}
|
||||
|
||||
s.lastStamp = stamp
|
||||
|
||||
// 组合 ID
|
||||
id := (stamp << 22) | (s.machineID << 12) | s.sequence
|
||||
return id
|
||||
}
|
||||
```
|
||||
|
||||
### 前端精度问题与解决方案
|
||||
|
||||
**问题现象**:
|
||||
``javascript
|
||||
// ❌ JavaScript 无法精确表示 20 位数字
|
||||
const backendID = 18446744073709551615;
|
||||
console.log(backendID);
|
||||
// 输出:18446744073709552000(精度丢失!)
|
||||
|
||||
Number.MAX_SAFE_INTEGER = 9007199254740991 // 仅 16 位
|
||||
```
|
||||
|
||||
**解决方案:后端使用 string 传输**
|
||||
|
||||
``go
|
||||
// internal/model/network.go
|
||||
type Network struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
|
||||
ID_UInt64 uint64 `gorm:"column:id_uint64;not null" json:"-"`
|
||||
}
|
||||
|
||||
// BeforeCreate Hook:uint64 → string
|
||||
func (n *Network) BeforeCreate(tx *gorm.DB) error {
|
||||
n.ID = strconv.FormatUint(n.ID_UInt64, 10)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AfterFind Hook:string ← uint64
|
||||
func (n *Network) AfterFind(tx *gorm.DB) error {
|
||||
n.ID_UInt64, _ = strconv.ParseUint(n.ID, 10, 64)
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
**前端使用**:
|
||||
``typescript
|
||||
// 前端始终处理字符串
|
||||
interface Network {
|
||||
id: string // "18446744073709551615"
|
||||
name: string
|
||||
}
|
||||
|
||||
// 不需要任何特殊处理
|
||||
const network = await api.getNetwork('18446744073709551615')
|
||||
console.log(network.id) // "18446744073709551615" ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Conn.Bind 模型与 9 层传输
|
||||
|
||||
### Bind 接口定义
|
||||
|
||||
WireGuard 官方库提供的网络层抽象接口:
|
||||
|
||||
```
|
||||
// golang.zx2c4.com/wireguard/conn/bind.go
|
||||
type Bind interface {
|
||||
Open(port uint16) (fns []ReceiveFunc, err error)
|
||||
Close() error
|
||||
SetMark(mark uint32) error
|
||||
Send(buffs [][]byte, ep Endpoint) error
|
||||
ParseEndpoint(addr string) (Endpoint, error)
|
||||
}
|
||||
```
|
||||
|
||||
**接口契约说明**:
|
||||
|
||||
| 方法 | 调用时机 | 返回值含义 |
|
||||
|------|---------|------------|
|
||||
| `Open` | WG 设备启动时 | `fns` 数组长度 = CPU 核心数(每核一个接收函数) |
|
||||
| `Close` | WG 设备停止时 | 必须释放所有资源(端口、goroutine) |
|
||||
| `SetMark` | Linux 特有 | 用于 iptables 路由策略 |
|
||||
| `Send` | WG 要发送密文包 | 返回 nil 表示已发出,非 nil 触发重试 |
|
||||
| `ParseEndpoint` | 添加 Peer 时 | 将字符串 "host:port" 转为 Endpoint 对象 |
|
||||
|
||||
---
|
||||
|
||||
### TransportFactory 接口定义
|
||||
|
||||
每种传输方式实现 `TransportFactory` 接口:
|
||||
|
||||
```
|
||||
// core/connect/factory.go
|
||||
type TransportFactory interface {
|
||||
// Layer 返回所属传输层(用于优先级排序)
|
||||
Layer() Layer
|
||||
|
||||
// Name 返回名称(用于日志和监控)
|
||||
Name() string
|
||||
|
||||
// Dial 拨号建立连接(核心方法)
|
||||
Dial(ctx context.Context, config *DialConfig) (net.Conn, error)
|
||||
}
|
||||
|
||||
// DialConfig 拨号配置(所有传输方式共享)
|
||||
type DialConfig struct {
|
||||
LocalAddr string // 本地地址
|
||||
RemoteAddr string // 对端地址
|
||||
PeerID string // 对端设备 ID
|
||||
Timeout time.Duration // 超时时间
|
||||
STUNServers []string // STUN 服务器列表(P2P 需要)
|
||||
TURNConfig *TURNConfig // TURN 配置(TURN 需要)
|
||||
}
|
||||
```
|
||||
|
||||
**接口契约说明**:
|
||||
|
||||
| 方法 | 作用 | 实现要求 |
|
||||
|------|------|---------|
|
||||
| `Layer()` | 返回所属层级 | 决定在 9 层中的位置(LayerDirectUDP/LayerTURN_UDP 等) |
|
||||
| `Name()` | 人类可读的名称 | 用于日志输出:"Direct-UDP" / "TURN-TCP" / "WS" |
|
||||
| `Dial()` | 建立连接 | 返回 net.Conn 接口,封装底层协议细节 |
|
||||
|
||||
---
|
||||
|
||||
### CoreBind 实现
|
||||
|
||||
```
|
||||
// core/core_bind.go
|
||||
type CoreBind struct {
|
||||
conn.Bind
|
||||
routes map[RouteID]net.Conn // Route ID → 实际链路
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Send 截获 WG 密文包,选择链路发送
|
||||
func (cb *CoreBind) Send(buffs [][]byte, ep conn.Endpoint) error {
|
||||
cb.mu.RLock()
|
||||
defer cb.mu.RUnlock()
|
||||
|
||||
packet := buffs[0]
|
||||
|
||||
// 数据包分类
|
||||
msgType := packet[0]
|
||||
if msgType == 1 || msgType == 2 || msgType == 3 {
|
||||
// 控制包(握手/Cookie):按已建链路透传
|
||||
return cb.sendControlPacket(packet, ep)
|
||||
}
|
||||
|
||||
if msgType == 4 {
|
||||
// 数据包:提取 Route ID 并查表
|
||||
routeID := binary.LittleEndian.Uint32(packet[4:8])
|
||||
link := cb.routes[routeID]
|
||||
|
||||
if link == nil {
|
||||
return fmt.Errorf("no route for %d", routeID)
|
||||
}
|
||||
|
||||
// 通过选定链路发送
|
||||
_, err := link.Write(packet)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 9 层传输工厂模式
|
||||
|
||||
每种传输方式实现 `TransportFactory` 接口:
|
||||
|
||||
```
|
||||
// core/connect/factory.go
|
||||
type TransportFactory interface {
|
||||
Layer() Layer
|
||||
Name() string
|
||||
Dial(ctx context.Context, config *DialConfig) (net.Conn, error)
|
||||
}
|
||||
```
|
||||
|
||||
#### 示例 1:Direct-UDP(STUN P2P)
|
||||
|
||||
```
|
||||
// core/connect/p2p_factory.go
|
||||
type P2PFactory struct {
|
||||
stunServers []string
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func (f *P2PFactory) Layer() Layer {
|
||||
return LayerDirectUDP
|
||||
}
|
||||
|
||||
func (f *P2PFactory) Name() string {
|
||||
return "Direct-UDP"
|
||||
}
|
||||
|
||||
func (f *P2PFactory) Dial(ctx context.Context, config *DialConfig) (net.Conn, error) {
|
||||
// 1. STUN 打洞获取公网地址
|
||||
publicAddr, err := stun.GetPublicAddr(f.stunServers...)
|
||||
|
||||
// 2. 交换候选地址(通过 ctr 信令中转)
|
||||
localCandidate := Candidate{Addr: publicAddr, Priority: 100}
|
||||
remoteCandidate := f.exchangeCandidates(config.PeerID, localCandidate)
|
||||
|
||||
// 3. 建立 P2P 连接
|
||||
conn, err := net.DialUDP("udp", nil, &remoteCandidate.Addr)
|
||||
|
||||
// 4. 包装为 P2PConn(带保活、NAT 刷新)
|
||||
return NewP2PConn(conn, f.logger), nil
|
||||
}
|
||||
```
|
||||
|
||||
#### 示例 2:TURN-UDP
|
||||
|
||||
```
|
||||
// core/connect/turn_factory.go
|
||||
type TURNFactory struct {
|
||||
serverConfig *TURNConfig
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func (f *TURNFactory) Dial(ctx context.Context, config *DialConfig) (net.Conn, error) {
|
||||
// 1. 连接 TURN 服务器
|
||||
turnConn, err := turnclient.New(config.ServerAddr, config.Username, config.Password)
|
||||
|
||||
// 2. 创建 Permission(允许对端 IP)
|
||||
turnConn.CreatePermission(config.RemoteAddr)
|
||||
|
||||
// 3. 返回 TURNConn(封装了 Allocate/Send/Refresh)
|
||||
return NewTURNConn(turnConn, f.logger), nil
|
||||
}
|
||||
```
|
||||
|
||||
#### 示例 3:FakeTCP(UDP 伪装 TCP)
|
||||
|
||||
```
|
||||
// core/connect/fake_tcp_factory.go
|
||||
type FakeTCPFactory struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func (f *FakeTCPFactory) Dial(ctx context.Context, config *DialConfig) (net.Conn, error) {
|
||||
// 1. 建立 TCP 连接
|
||||
tcpConn, err := net.DialTCP("tcp", nil, config.RemoteAddr)
|
||||
|
||||
// 2. 将 UDP 包封装为 TCP 流(带长度前缀)
|
||||
// [4 bytes length][UDP packet...]
|
||||
return NewFakeTCPConn(tcpConn, f.logger), nil
|
||||
}
|
||||
```
|
||||
|
||||
### 策略调度器
|
||||
|
||||
```
|
||||
// core/connect/strategy.go
|
||||
type StrategyScheduler struct {
|
||||
factories map[Layer]TransportFactory
|
||||
priority []Layer // 优先级顺序
|
||||
}
|
||||
|
||||
// TryAll 按优先级尝试所有层,直到成功
|
||||
func (s *StrategyScheduler) TryAll(ctx context.Context, config *DialConfig) (net.Conn, error) {
|
||||
var lastErr error
|
||||
|
||||
for _, layer := range s.priority {
|
||||
factory := s.factories[layer]
|
||||
if factory == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
conn, err := factory.Dial(ctx, config)
|
||||
if err == nil {
|
||||
// 成功,记录日志并返回
|
||||
s.logger.Info("connected via " + factory.Name())
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
s.logger.Debug(layer.String() + " failed: " + err.Error())
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("all layers failed: %w", lastErr)
|
||||
}
|
||||
```
|
||||
|
||||
### 传输状态机
|
||||
|
||||
```
|
||||
初始状态 ──→ Layer 1 (Direct-UDP)
|
||||
├─ 成功 → 保持(持续监控)
|
||||
├─ 超时 (500ms) → Layer 2 (FakeTCP)
|
||||
└─ 丢包率 > 10% → Layer 2
|
||||
|
||||
Layer 2 (FakeTCP) ──→ Layer 3 (RealTCP)
|
||||
├─ 成功 → 保持
|
||||
└─ 失败 → Layer 4
|
||||
|
||||
... 依次降级 ...
|
||||
|
||||
Layer 8 (WS/WSS) ──→ 降级完成 → 启动恢复探测
|
||||
│
|
||||
├─ 每 30 秒探测 Layer 1
|
||||
├─ 连续 2 次成功 → 切回 Layer 1
|
||||
└─ 失败 → 继续等待下一轮探测
|
||||
```
|
||||
|
||||
**状态转换规则**:
|
||||
|
||||
| 当前状态 | 触发条件 | 目标状态 |
|
||||
|----------|---------|---------|
|
||||
| Layer N | 握手成功 | 保持 Layer N |
|
||||
| Layer N | 单包超时 > 500ms | Layer N+1 |
|
||||
| Layer N | 丢包率 > 10% (10s 窗口) | Layer N+1 |
|
||||
| Layer N+1 | 建立成功 | 保持 Layer N+1 |
|
||||
| Layer 9 (WS) | 恢复探测成功×2 | Layer 1 |
|
||||
| Layer 9 (WS) | 恢复探测失败 | 保持 Layer 9 |
|
||||
|
||||
### 自动切换机制
|
||||
|
||||
```
|
||||
// 降级触发条件
|
||||
type FallbackPolicy struct {
|
||||
PacketTimeout time.Duration // 单包超时:500ms
|
||||
LossRateWindow time.Duration // 丢包率窗口:10s
|
||||
LossRateThreshold float64 // 丢包率阈值:10%
|
||||
}
|
||||
|
||||
// 恢复探测
|
||||
func (s *StrategyScheduler) StartRecoveryProbe(currentLayer Layer) {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
|
||||
go func() {
|
||||
for range ticker.C {
|
||||
// 每隔 30 秒向 Layer 1 发送探测包
|
||||
success := s.probeLayer(LayerDirectUDP)
|
||||
|
||||
if success {
|
||||
// 连续 2 次成功 → 直接切回 Layer 1
|
||||
s.switchTo(LayerDirectUDP)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Mesh 中继架构
|
||||
|
||||
### 核心设计原则
|
||||
|
||||
**Mesh 中继不是 Core 的功能,而是 WG 设备层的静态路由拓扑配置。**
|
||||
|
||||
### 数据流向
|
||||
|
||||
```
|
||||
A (10.0.0.2) ──→ C (10.0.0.1) ──→ B (10.0.0.3)
|
||||
↑
|
||||
中继节点
|
||||
|
||||
WG 视角:
|
||||
A 的 WG 配置:Peer C, AllowedIPs = 10.0.0.0/24
|
||||
B 的 WG 配置:Peer C, AllowedIPs = 10.0.0.0/24
|
||||
C 的 WG 配置:Peer A (AllowedIPs=10.0.0.2/32), Peer B (AllowedIPs=10.0.0.3/32)
|
||||
|
||||
Core 视角:
|
||||
A.Core: Direct-UDP → C.Core
|
||||
B.Core: Direct-UDP → C.Core
|
||||
C.Core: 无感知,只是转发密文包
|
||||
```
|
||||
|
||||
### ctr 配置中继节点
|
||||
|
||||
```
|
||||
// internal/ctr/wg.go
|
||||
func (c *Ctr) ConfigureRelay(relayDevice *model.Device, peers []*model.Device) error {
|
||||
wgConfig := wgtypes.Config{}
|
||||
|
||||
// 为中继节点配置所有普通节点作为 Peer
|
||||
for _, peer := range peers {
|
||||
wgConfig.Peers = append(wgConfig.Peers, wgtypes.PeerConfig{
|
||||
PublicKey: peer.PublicKey,
|
||||
AllowedIPs: []net.IPNet{
|
||||
peer.VirtualIP, // 10.0.0.3/32
|
||||
peer.LocalSubnet, // 192.168.1.0/24(可选)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 调用 wgctrl 应用配置
|
||||
return c.wgClient.ConfigureDevice(relayDevice.WGInterface, wgConfig)
|
||||
}
|
||||
```
|
||||
|
||||
### 系统要求(重要)
|
||||
|
||||
**启用 Mesh 中继前,必须在操作系统层面开启 IP 转发**:
|
||||
|
||||
#### Linux 系统
|
||||
|
||||
```
|
||||
# 1. 临时开启 IPv4 转发
|
||||
sudo sysctl -w net.ipv4.ip_forward=1
|
||||
|
||||
# 2. 永久开启
|
||||
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
|
||||
sysctl -p
|
||||
|
||||
# 3. 配置 iptables NAT 转发(必需!)
|
||||
sudo iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
|
||||
sudo iptables -A FORWARD -i wg0 -j ACCEPT
|
||||
sudo iptables -A FORWARD -o wg0 -j ACCEPT
|
||||
|
||||
# 4. 保存 iptables 规则
|
||||
sudo apt install iptables-persistent
|
||||
sudo netfilter-persistent save
|
||||
```
|
||||
|
||||
#### Docker 容器
|
||||
|
||||
```
|
||||
docker run \
|
||||
--privileged \ # 必需:特权模式
|
||||
--sysctl net.ipv4.ip_forward=1 \ # 必需:开启 IP 转发
|
||||
--cap-add=NET_ADMIN \ # 推荐:网络管理权限
|
||||
meshray-node:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. ExternalService 服务市场
|
||||
|
||||
### 架构设计理念
|
||||
|
||||
**为什么需要三层架构?**
|
||||
|
||||
| 问题 | 单层方案 | 三层方案 |
|
||||
|------|---------|----------|
|
||||
| **配置千差万别** | ❌ 需要 N 张表 | ✅ JSON 一张表 |
|
||||
| **如何防止乱填** | ❌ 手动校验每个字段 | ✅ Schema 自动验证 |
|
||||
| **前端如何知道填什么** | ❌ 硬编码表单 | ✅ 动态渲染 |
|
||||
| **后端类型安全** | ❌ map[string]interface{} | ✅ Go Struct |
|
||||
|
||||
### 三层架构详解
|
||||
|
||||
#### 第一层:JSON 存储
|
||||
|
||||
```
|
||||
type ExternalService struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
|
||||
Category string `gorm:"type:varchar(32);index" json:"category"`
|
||||
ServiceType string `gorm:"type:varchar(64);index" json:"serviceType"`
|
||||
Name string `gorm:"type:varchar(64)" json:"name"`
|
||||
Tags string `gorm:"type:varchar(255)" json:"tags"` // JSON 数组
|
||||
Config string `gorm:"type:text;not null" json:"config"` // ← JSON 字符串
|
||||
Enabled bool `gorm:"default:true;index" json:"enabled"`
|
||||
}
|
||||
```
|
||||
|
||||
**示例数据**:
|
||||
```
|
||||
{
|
||||
"category": "networking",
|
||||
"serviceType": "frp_server",
|
||||
"name": "我的 FRP 服务器",
|
||||
"config": "{\"server_addr\":\"frp.example.com\",\"token\":\"xxx\"}"
|
||||
}
|
||||
```
|
||||
|
||||
#### 第二层:Schema 验证
|
||||
|
||||
**FRP 服务器配置 Schema**:
|
||||
```
|
||||
func (p *FRPServerProvider) ConfigSchema() string {
|
||||
return `{
|
||||
"type": "object",
|
||||
"required": ["server_addr", "token"],
|
||||
"properties": {
|
||||
"server_addr": {
|
||||
"type": "string",
|
||||
"title": "FRP 服务器地址"
|
||||
},
|
||||
"server_port": {
|
||||
"type": "integer",
|
||||
"default": 7000,
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
},
|
||||
"token": {
|
||||
"type": "string",
|
||||
"format": "password",
|
||||
"minLength": 8
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
"enum": ["tcp", "kcp", "quic"],
|
||||
"default": "tcp"
|
||||
}
|
||||
}
|
||||
}`
|
||||
}
|
||||
```
|
||||
|
||||
#### 第三层:Struct 类型转换
|
||||
|
||||
```
|
||||
type FRPConfig struct {
|
||||
ServerAddr string `json:"server_addr"`
|
||||
ServerPort int `json:"server_port,omitempty"`
|
||||
Token string `json:"token"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
}
|
||||
|
||||
func (p *FRPServerProvider) ValidateConfig(configJSON string) error {
|
||||
var cfg FRPConfig
|
||||
json.Unmarshal([]byte(configJSON), &cfg)
|
||||
|
||||
if cfg.ServerAddr == "" {
|
||||
return errors.New("服务器地址不能为空")
|
||||
}
|
||||
if cfg.Token == "" {
|
||||
return errors.New("认证令牌不能为空")
|
||||
}
|
||||
if len(cfg.Token) < 8 {
|
||||
return errors.New("认证令牌长度至少 8 位")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *FRPServerProvider) BuildConfig(configJSON string) (*FRPConfig, error) {
|
||||
var cfg FRPConfig
|
||||
json.Unmarshal([]byte(configJSON), &cfg)
|
||||
|
||||
// 设置默认值
|
||||
if cfg.ServerPort == 0 {
|
||||
cfg.ServerPort = 7000
|
||||
}
|
||||
if cfg.Protocol == "" {
|
||||
cfg.Protocol = "tcp"
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 复杂场景支持
|
||||
|
||||
#### TURN 服务器多种认证方式
|
||||
|
||||
```
|
||||
type TURNConfig struct {
|
||||
ServerAddr string `json:"server_addr"`
|
||||
Realm string `json:"realm,omitempty"`
|
||||
|
||||
// 认证方式(互斥)
|
||||
AuthType string `json:"auth_type"` // long_term | short_term | auth_secret
|
||||
LongTerm *LongTermAuth `json:"long_term,omitempty"`
|
||||
ShortTerm *ShortTermAuth `json:"short_term,omitempty"`
|
||||
}
|
||||
|
||||
type LongTermAuth struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type ShortTermAuth struct {
|
||||
Username string `json:"username"`
|
||||
AuthSecret string `json:"auth_secret"`
|
||||
ExpiresIn int `json:"expires_in,omitempty"`
|
||||
}
|
||||
|
||||
// GetCredentials 动态获取凭证
|
||||
func (p *TURNServerProvider) GetCredentials(configJSON string) (Credentials, error) {
|
||||
var cfg TURNConfig
|
||||
json.Unmarshal([]byte(configJSON), &cfg)
|
||||
|
||||
switch cfg.AuthType {
|
||||
case "long_term":
|
||||
return &LongTermCredentials{
|
||||
Username: cfg.LongTerm.Username,
|
||||
Password: cfg.LongTerm.Password,
|
||||
}, nil
|
||||
|
||||
case "short_term":
|
||||
// 动态生成短期凭证(HMAC-SHA1)
|
||||
return p.generateShortTermCredentials(cfg.ShortTerm)
|
||||
|
||||
case "auth_secret":
|
||||
return p.generateAuthSecretCredentials(cfg.AuthSecret)
|
||||
|
||||
default:
|
||||
return nil, errors.New("不支持的认证方式")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### WS 隧道多种部署方式
|
||||
|
||||
```
|
||||
type WSTunnelConfig struct {
|
||||
// 服务器类型(仅用于 UI 分类)
|
||||
ServerType string `json:"server_type"` // coturn | nginx | standalone
|
||||
|
||||
// 统一的 WebSocket 端点
|
||||
EndpointType string `json:"endpoint_type"` // ws | wss
|
||||
Endpoint string `json:"endpoint"` // ws://example.com:80/ws
|
||||
|
||||
// 是否关联 STUN(如果是 coturn)
|
||||
UseCoturnSTUN bool `json:"use_coturn_stun"`
|
||||
STUNEndpoint string `json:"stun_endpoint,omitempty"`
|
||||
}
|
||||
|
||||
func (p *WSTunnelProvider) BuildTransportConfig(configJSON string) (*TransportConfig, error) {
|
||||
var cfg WSTunnelConfig
|
||||
json.Unmarshal([]byte(configJSON), &cfg)
|
||||
|
||||
u, _ := url.Parse(cfg.Endpoint)
|
||||
|
||||
return &TransportConfig{
|
||||
Type: "ws_tunnel",
|
||||
Protocol: u.Scheme,
|
||||
Host: u.Host,
|
||||
Path: u.Path,
|
||||
|
||||
// 如果是 coturn 且启用了 STUN,自动注入 STUN 配置
|
||||
STUNServers: func() []string {
|
||||
if cfg.UseCoturnSTUN && cfg.STUNEndpoint != "" {
|
||||
return []string{cfg.STUNEndpoint}
|
||||
}
|
||||
return nil
|
||||
}(),
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. MeshSeed 安全机制
|
||||
|
||||
### 双层安全设计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 外层:AES-256-GCM 加密 │
|
||||
│ - 保护机密性 │
|
||||
│ - 防止偷看 DNS 记录 │
|
||||
│ - 密钥派生:SHA256("meshray-ddns" + NetworkSecret) │
|
||||
└─────────────────────────────────────┘
|
||||
↓ 包裹着
|
||||
┌─────────────────────────────────────┐
|
||||
│ 内层:Ed25519 签名 │
|
||||
│ - 保护完整性与来源 │
|
||||
│ - 防止中间人篡改 │
|
||||
│ - 公钥从 seed.IssuerNodeID 解析 │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 加密流程
|
||||
|
||||
```
|
||||
// pkg/meshseed/crypto.go
|
||||
|
||||
// 1. 派生 AES-256-GCM 密钥
|
||||
func deriveKey(networkSecret string) []byte {
|
||||
hash := sha256.Sum256([]byte("meshray-ddns" + networkSecret))
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
// 2. 加密 MeshSeed
|
||||
func Encrypt(seed *MeshSeed, networkSecret string) (string, error) {
|
||||
// 签名(如果还没有)
|
||||
if len(seed.Signature) == 0 {
|
||||
return "", fmt.Errorf("MeshSeed 未签名")
|
||||
}
|
||||
|
||||
// 序列化
|
||||
plaintext, _ := json.Marshal(seed)
|
||||
|
||||
// 派生密钥
|
||||
key := deriveKey(networkSecret)
|
||||
|
||||
// AES-GCM 加密
|
||||
block, _ := aes.NewCipher(key)
|
||||
gcm, _ := cipher.NewGCM(block)
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
rand.Read(nonce)
|
||||
|
||||
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
|
||||
|
||||
// Base64URL 编码
|
||||
return base64.URLEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// 3. 解密 MeshSeed
|
||||
func Decrypt(encrypted string, networkSecret string) (*MeshSeed, error) {
|
||||
// Base64URL 解码
|
||||
ciphertext, _ := base64.URLEncoding.DecodeString(encrypted)
|
||||
|
||||
// 派生密钥
|
||||
key := deriveKey(networkSecret)
|
||||
|
||||
// AES-GCM 解密
|
||||
block, _ := aes.NewCipher(key)
|
||||
gcm, _ := cipher.NewGCM(block)
|
||||
nonceSize := gcm.NonceSize()
|
||||
|
||||
nonce, ciphertextBytes := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
||||
plaintext, _ := gcm.Open(nil, nonce, ciphertextBytes, nil)
|
||||
|
||||
// 反序列化
|
||||
var seed MeshSeed
|
||||
json.Unmarshal(plaintext, &seed)
|
||||
|
||||
return &seed, nil
|
||||
}
|
||||
```
|
||||
|
||||
### DDNS TXT 记录格式
|
||||
|
||||
```
|
||||
# DNS TXT 记录
|
||||
_meshray.home.example.com. IN TXT "meshray-ddns:<base64_url_encoded_data>"
|
||||
|
||||
# 数据结构
|
||||
base64_url_encoded_data = nonce (12 字节) + ciphertext + tag (16 字节)
|
||||
```
|
||||
|
||||
### 完整使用流程
|
||||
|
||||
#### Publish 发布 MeshSeed
|
||||
|
||||
```
|
||||
// internal/provider/sync/libdns_adapter.go
|
||||
|
||||
func (a *LibdnsAdapter) Publish(configJSON string, seed *meshseed.MeshSeed) error {
|
||||
// 1. 解析配置
|
||||
var cfg LibdnsConfig
|
||||
json.Unmarshal([]byte(configJSON), &cfg)
|
||||
|
||||
// 2. 加密 MeshSeed
|
||||
encrypted, err := meshseed.Encrypt(seed, seed.NetworkSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("加密失败:%w", err)
|
||||
}
|
||||
|
||||
// 3. 构造 TXT 记录值
|
||||
txtValue := "meshray-ddns:" + encrypted
|
||||
|
||||
// 4. 写入 DNS TXT 记录
|
||||
ctx := context.Background()
|
||||
_, err = a.appender.AppendRecords(ctx, cfg.Domain, []libdns.Record{
|
||||
libdns.RR{
|
||||
Type: "TXT",
|
||||
Name: cfg.TXTRecordName,
|
||||
Data: txtValue,
|
||||
TTL: time.Duration(cfg.TTL) * time.Second,
|
||||
},
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
#### Fetch 拉取 MeshSeed
|
||||
|
||||
```
|
||||
func (a *LibdnsAdapter) Fetch(configJSON string, networkID uint64) (*meshseed.MeshSeed, error) {
|
||||
// 1. 解析配置
|
||||
var cfg LibdnsConfig
|
||||
json.Unmarshal([]byte(configJSON), &cfg)
|
||||
|
||||
// 2. 读取 DNS TXT 记录
|
||||
ctx := context.Background()
|
||||
records, _ := a.getter.GetRecords(ctx, cfg.Domain)
|
||||
|
||||
// 3. 查找匹配的记录
|
||||
for _, record := range records {
|
||||
rr := record.RR()
|
||||
if rr.Type != "TXT" || rr.Name != cfg.TXTRecordName {
|
||||
continue
|
||||
}
|
||||
|
||||
// 4. 提取加密数据
|
||||
txtValue := rr.Data
|
||||
encrypted := strings.TrimPrefix(txtValue, "meshray-ddns:")
|
||||
|
||||
// 5. 解密 MeshSeed(需要 NetworkSecret)
|
||||
seed, err := meshseed.Decrypt(encrypted, networkSecret)
|
||||
if err != nil {
|
||||
continue // 密钥不匹配或数据被篡改
|
||||
}
|
||||
|
||||
// 6. 验证 NetworkID
|
||||
if seed.NetworkID != networkID {
|
||||
continue
|
||||
}
|
||||
|
||||
// 7. Ed25519 验签
|
||||
if err := meshseed.Verify(seed, nil); err != nil {
|
||||
continue // 签名无效
|
||||
}
|
||||
|
||||
return seed, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("未找到 MeshSeed 记录")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*最后更新:v2.1.0*
|
||||
Reference in New Issue
Block a user