24 KiB
MeshRay 关键技术详解
目录
- [雪花算法 ID 生成](#1-雪花算法 id 生成)
- [Conn.Bind 模型与 9 层传输](#2-connbind-模型与 9 层传输)
- Mesh 中继架构
- ExternalService 服务市场
- MeshSeed 安全机制
1. 雪花算法 ID 生成
算法结构
64 位整数:
┌─┬──────────────────────┬────────────┬─────────────┐
│0│ 41 bits 时间戳 │ 10 bits │ 12 bits │
│ │ (毫秒级,69 年) │ 节点 ID │ 序列号 │
└─┴──────────────────────┴────────────┴─────────────┘
↑ ↑ ↑
符号位 机器标识 单毫秒内自增
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*