Documentation
¶
Index ¶
- Constants
- Variables
- func AppendFrame(b []byte, t FrameType, flags uint8, streamID uint64, payload []byte, pad int) []byte
- func AppendVarint(b []byte, v uint64) []byte
- func DefaultHandler(ctx context.Context, st *Stream)
- func DefaultPacketHandler(ctx context.Context, ps *PacketStream)
- func ReadVarint(b []byte) (v uint64, n int)
- func UserIDFromPassword(password string) [16]byte
- func VarintLen(v uint64) int
- type Client
- func (c *Client) Close() error
- func (c *Client) CloseSession()
- func (c *Client) CurrentSession() *Session
- func (c *Client) DialContext(ctx context.Context, network, addr string) (net.Conn, error)
- func (c *Client) DialPacket(ctx context.Context, addr string) (*PacketStream, error)
- func (c *Client) Session(ctx context.Context) (*Session, error)
- func (c *Client) TicketsRemaining() int
- func (c *Client) TicketsTaken() uint64
- type ClientConfig
- type Datagram
- type DialFunc
- type Frame
- type FrameType
- type MemTicketStore
- func (s *MemTicketStore) Consume(user [16]byte, id uint64) ([32]byte, bool)
- func (s *MemTicketStore) ConsumeAny(id uint64) ([32]byte, [16]byte, bool)
- func (s *MemTicketStore) Issue(user [16]byte, count uint16) (uint64, [32]byte, error)
- func (s *MemTicketStore) Remaining(user [16]byte) int
- func (s *MemTicketStore) Sweep(now time.Time)
- type PacketStream
- func (ps *PacketStream) Assoc() uint64
- func (ps *PacketStream) Close() error
- func (ps *PacketStream) LocalAddr() net.Addr
- func (ps *PacketStream) ReadFrom() (*Datagram, error)
- func (ps *PacketStream) SetReadDeadline(t time.Time) error
- func (ps *PacketStream) WriteTo(b []byte, addr string) (int, error)
- type PaddingPhase
- type PathInfo
- type PrivateKey
- type PublicKey
- type Server
- type ServerConfig
- type Session
- func (s *Session) AcceptStream(ctx context.Context) (*Stream, error)
- func (s *Session) Close() error
- func (s *Session) Done() <-chan struct{}
- func (s *Session) ID() [16]byte
- func (s *Session) KillAllPaths() int
- func (s *Session) LocalAddr() net.Addr
- func (s *Session) OpenPacket(ctx context.Context, dst string) (*PacketStream, error)
- func (s *Session) OpenStream(ctx context.Context, dst string) (*Stream, error)
- func (s *Session) PathCount() int
- func (s *Session) Paths() []PathInfo
- func (s *Session) PathsEstablished() uint64
- type Stream
- func (st *Stream) Close() error
- func (st *Stream) CloseWrite() error
- func (st *Stream) ID() uint64
- func (st *Stream) LocalAddr() net.Addr
- func (st *Stream) Read(p []byte) (int, error)
- func (st *Stream) RemoteAddr() net.Addr
- func (st *Stream) SetDeadline(t time.Time) error
- func (st *Stream) SetReadDeadline(t time.Time) error
- func (st *Stream) SetWriteDeadline(t time.Time) error
- func (st *Stream) Write(p []byte) (int, error)
- type StreamError
- type TicketStore
Constants ¶
const ( // DefaultProbeInterval:有活跃流时的路径探测间隔。1s 不是为了测 RTT—— // 是为了**尽早发现路径死了**。探测间隔直接决定故障检测下界,而检测越晚, // 用户看到的卡顿越长。空闲时会自动退到 DefaultIdleProbeInterval。 DefaultProbeInterval = 1 * time.Second DefaultIdleProbeInterval = 15 * time.Second // DefaultProbeTimeout:单次探测判定丢失的时限。取 max(3×SRTT, 这个值), // 所以高延迟线路不会被误杀。 DefaultProbeTimeout = 2 * time.Second // DefaultPathDeadAfter:连续多久收不到任何字节就判定路径死亡。 DefaultPathDeadAfter = 8 * time.Second // DefaultSessionGrace:会话在**一条路径都没有**的情况下能存活多久。 // 这是整个协议对网络波动最重要的一个数:Wi-Fi 切换、蜂窝换基站、路由器重启 // 都在这个窗口内,用户的 TCP 连接一条都不会断。代价是服务端要在这段时间里 // 替一个已经不在线的客户端持有上游连接与缓冲区。 DefaultSessionGrace = 120 * time.Second // DefaultStreamWindow:单流发送窗口,同时也是重传缓冲上限。 // 迁移时要重发 [acked, sent) 的全部字节,所以这个数直接决定一次迁移最坏要重传多少。 // 512 KiB 在 100 Mbps × 40ms 的 BDP(约 500 KiB)附近,既不限速也不会让一次 // 迁移拖出几秒的重传。 DefaultStreamWindow = 512 * 1024 // DefaultSessionWindow:会话级接收缓冲上限,防止大量流各自吃满窗口把内存打爆。 DefaultSessionWindow = 8 * 1024 * 1024 DefaultMaxStreams = 1024 )
默认值集中在这里,方便一眼看全"网络波动下的行为"由哪些常数决定。
const ( ProtocolVersion uint8 = 0x01 ChannelBindingLabel = "tide-channel-binding" )
版本字节。draft-01 相对 spec.md 的 draft-00 有三处线格式变化,见 spec.md §11 变更记录: STREAM_DATA 带绝对 offset、新增 STREAM_ACK/TICKET_REQUEST、填充长度改为尾部 u16。
const ( FlagPad uint8 = 1 << 0 FlagEnd uint8 = 1 << 1 FlagPush uint8 = 1 << 2 )
标志位(spec §2.2)。
const ( DefaultTicketCount = 1024 TicketLifetime = 24 * time.Hour )
const MaxFrameBody = 56 * 1024
MaxFrameBody 是 length 字段允许的上限。定成 56 KiB 而不是 varint 的 2^62: 一个恶意对端只要声明一个巨大的 length 就能让接收方预分配等量内存, 上限是这里唯一的防线。上界还必须让一帧装得进一条 sealed 记录(见 record.go 的 maxRecordPlain),否则批量阶段会出现"帧合法但永远发不出去"的死角。
const MaxPayload = 16 * 1024
MaxPayload 是单个 STREAM_DATA 的载荷上限。16 KiB 对齐 TLS 记录最大明文长度, 让"一帧 = 一条 TLS 记录"在批量阶段成立,避免一帧被拆进两条记录后产生额外的包长特征。
const MaxVarint = uint64(1)<<62 - 1
const TimestampTolerance = 120 * time.Second
TimestampTolerance 是 spec §3.1 的 ±120 秒容差。
Variables ¶
var ( ErrClosed = errors.New("tide: session closed") ErrStreamClosed = errors.New("tide: stream closed") ErrStreamReset = errors.New("tide: stream reset by peer") ErrProtocol = errors.New("tide: protocol violation") ErrFrameTooLarge = errors.New("tide: frame exceeds max size") ErrBadTicket = errors.New("tide: ticket invalid or already consumed") ErrTicketExhausted = errors.New("tide: no unconsumed tickets left") ErrChannelBinding = errors.New("tide: channel binding mismatch") ErrStaleTimestamp = errors.New("tide: timestamp outside tolerance window") ErrVersion = errors.New("tide: unsupported version") ErrNoPath = errors.New("tide: no usable path") ErrSessionGone = errors.New("tide: session grace period expired") ErrFlowControl = errors.New("tide: peer exceeded flow-control window") ErrTooManyStreams = errors.New("tide: stream limit reached") )
协议级错误。注意:这些**不会**被发到线上——§6 的失败关闭要求任何认证失败都不产生 可区分的响应。它们只用于本地日志与调用方决策。
Functions ¶
func AppendFrame ¶
func AppendFrame(b []byte, t FrameType, flags uint8, streamID uint64, payload []byte, pad int) []byte
AppendFrame 把一帧序列化到 b。pad 为附加的填充字节数(0 表示不填充)。
func AppendVarint ¶
AppendVarint 把 v 追加到 b。超范围时 panic——这是编码器内部约束, 所有调用点的取值都在协议定义的范围内,运行期出现只可能是逻辑错误。
func DefaultPacketHandler ¶
func DefaultPacketHandler(ctx context.Context, ps *PacketStream)
DefaultPacketHandler 为一条 UDP 关联建一个本地 socket 并转发。
func ReadVarint ¶
ReadVarint 从 b 头部解出一个 varint,返回值与消耗的字节数。 字节不足时返回 n=0——调用方据此判断"还要继续收",而不是报错。
func UserIDFromPassword ¶
UserIDFromPassword 把任意长度的口令折成 16 字节 user_id。
直接截断口令是不行的:短口令会留下尾部零字节,长口令则丢掉后半段—— 两个前 16 字节相同的口令会塌成同一个用户,而且不会有任何报错, 表现为 A 的流量被记在 B 头上、或者 A 能用 B 的票据。 用 SHA-256 折叠保证任意两个不同口令映射到同一个 id 的概率可忽略。
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
func NewClient ¶
func NewClient(cfg *ClientConfig) (*Client, error)
func (*Client) CloseSession ¶
func (c *Client) CloseSession()
CloseSession 关掉当前会话但保留客户端(票据钱包留着)。 下一次拨号会用 0-RTT 重新起一个会话——这是验证 0-RTT 复用最直接的办法。
func (*Client) CurrentSession ¶
CurrentSession 返回当前会话(可能为 nil)。
func (*Client) DialContext ¶
DialContext 开一条代理 TCP 流。签名与 net.Dialer.DialContext 兼容, 方便直接塞进 clash 的 dialer 链。
func (*Client) DialPacket ¶
DialPacket 开一条 UDP 关联。
func (*Client) TicketsRemaining ¶
TicketsRemaining 返回钱包里还剩多少张可用票据。
func (*Client) TicketsTaken ¶
TicketsTaken 返回累计用掉多少张票据。等于"有多少次握手是 0-RTT"。
type ClientConfig ¶
type ClientConfig struct {
// Server 是 host:port。
Server string
// PublicKey 是服务端静态公钥(base64,见 PublicKey 文档说明为什么这么长)。
PublicKey *PublicKey
// UserID 16 字节用户标识。
UserID [16]byte
// ServerName 外层 TLS SNI;空则取 Server 的 host。
ServerName string
// TLSConfig 可选,用于自定义外层 TLS(ALPN、指纹、跳过校验等)。
TLSConfig *tls.Config
// Bare 请求裸帧模式(内层不加密,安全性完全由外层 TLS 承担)。
// 服务端只在信道绑定校验通过时才会同意。
Bare bool
// EnableQUIC 允许调度器探测并使用 QUIC 路径。
EnableQUIC bool
// QUICPort 为 0 时复用 Server 的端口。
QUICPort int
// Redundancy 常驻维持两条路径。
//
// ★ 这是对抗网络波动最有效、也最贵的一个开关:路径死亡时不需要重连,
// 流直接切到另一条已经握好手的路径上,用户侧几乎无感(一个 RTT 内)。
// 代价是两条连接的保活开销,以及服务端两倍的会话资源。
// 弱网/移动网络建议开,稳定有线网不需要。
Redundancy bool
// SessionGrace 覆盖 DefaultSessionGrace。
SessionGrace time.Duration
// ProbeInterval 覆盖 DefaultProbeInterval。
ProbeInterval time.Duration
// StreamWindow 覆盖 DefaultStreamWindow。
StreamWindow uint64
// Dial 允许注入自定义拨号(clash 侧用来走 dialer 的接口绑定/DNS)。
Dial DialFunc
// Congestion 指定 TCP 路径的拥塞控制算法(Linux 专有,如 "bbr"、"cubic")。
// 空 = **不动系统默认**。填 "-" 同义。
//
// ⚠️ 别想当然地填 "bbr":实测双向 5% 丢包下它把 p99 从 125ms 抬到 620ms
// (详见 congestion_linux.go)。Linux 的 `bbr` 是 v1,对随机丢包的处理
// 正是它的弱点;design.md 里说的 BBRv3 内核里并没有。
// ⚠️ 只作用于 TCP 路径:quic-go v0.61 内置只有 cubic 且不暴露选择接口。
Congestion string
}
ClientConfig 是出站配置。
type FrameType ¶
type FrameType uint8
const ( FrameHello FrameType = 0x01 FrameAccept FrameType = 0x02 FrameZeroRTT FrameType = 0x03 FrameStreamOpen FrameType = 0x10 FrameStreamData FrameType = 0x11 FrameStreamFin FrameType = 0x12 FrameStreamRst FrameType = 0x13 FrameStreamAck FrameType = 0x14 // draft-01 新增,见 spec §2.1 注 FrameDatagram FrameType = 0x20 FramePathProbe FrameType = 0x30 FramePathAck FrameType = 0x31 FramePathMigrate FrameType = 0x32 FrameTicketRepl FrameType = 0x40 FrameTicketReq FrameType = 0x41 // draft-01 新增 FramePadding FrameType = 0x50 FrameClose FrameType = 0x5F )
type MemTicketStore ¶
type MemTicketStore struct {
// contains filtered or unexported fields
}
MemTicketStore 是单进程实现。
除了 per-user 的批次链,它还维护一份**按 base 排序的全局批次索引**。 这是为了解开 0-RTT 里的一个鸡生蛋:服务端要先查位图才能解密, 而用户身份就在待解密的密文里。全局基址单调分配 ⇒ ticket_id 本身唯一确定批次 ⇒ 可以先按 id 反查到批次(连带用户),再原子消费,最后才解密。 集群实现要么复制这个性质(全局单调发号),要么把 user_id 明文放进 ZERO_RTT—— 后者会泄露用户标识,不可接受。
func NewMemTicketStore ¶
func NewMemTicketStore() *MemTicketStore
func (*MemTicketStore) Consume ¶
func (s *MemTicketStore) Consume(user [16]byte, id uint64) ([32]byte, bool)
func (*MemTicketStore) ConsumeAny ¶
func (s *MemTicketStore) ConsumeAny(id uint64) ([32]byte, [16]byte, bool)
ConsumeAny 是 anyConsumer 的导出版本,让外部 store 实现可以照抄这个契约。
func (*MemTicketStore) Remaining ¶
func (s *MemTicketStore) Remaining(user [16]byte) int
func (*MemTicketStore) Sweep ¶
func (s *MemTicketStore) Sweep(now time.Time)
type PacketStream ¶
type PacketStream struct {
// contains filtered or unexported fields
}
PacketStream 是一条 UDP 关联,实现 net.PacketConn 的常用子集。
func (*PacketStream) Close ¶
func (ps *PacketStream) Close() error
func (*PacketStream) LocalAddr ¶
func (ps *PacketStream) LocalAddr() net.Addr
func (*PacketStream) ReadFrom ¶
func (ps *PacketStream) ReadFrom() (*Datagram, error)
ReadFrom 收一个数据报。
func (*PacketStream) SetReadDeadline ¶
func (ps *PacketStream) SetReadDeadline(t time.Time) error
type PaddingPhase ¶
type PaddingPhase uint8
PaddingPhase 供日志与自检观察当前处于哪一阶段。
const ( PhaseDecision PaddingPhase = iota // 判决窗口:每帧填到采样目标长度 PhaseDecay // 衰减:填充概率线性降到 0 PhaseBulk // 批量:不填充 PhaseOff // 填充被关闭(bare 模式:用户态不碰载荷) )
func (PaddingPhase) String ¶
func (p PaddingPhase) String() string
type PathInfo ¶
type PathInfo struct {
ID uint32
Kind string // "tcp" / "quic"
State string // active / degraded / suspect / dead
RTT time.Duration
Loss float64
Age time.Duration
Pad string // 当前填充阶段
TX uint64
RX uint64
}
PathInfo 是一条路径的可观测状态。
type PrivateKey ¶
type PrivateKey struct {
// contains filtered or unexported fields
}
PrivateKey 是服务端长期密钥:X25519 静态私钥 + ML-KEM-768 解封装私钥。
func ParsePrivateKey ¶
func ParsePrivateKey(s string) (*PrivateKey, error)
ParsePrivateKey 解析 Seed()/String() 的产物。
func (*PrivateKey) Seed ¶
func (k *PrivateKey) Seed() []byte
Seed 序列化私钥为 96 字节(32 X25519 + 64 ML-KEM 种子)。
func (*PrivateKey) String ¶
func (k *PrivateKey) String() string
type PublicKey ¶
type PublicKey struct {
// contains filtered or unexported fields
}
PublicKey 是客户端配置里那一大坨 base64:X25519 公钥(32) || ML-KEM-768 封装公钥(1184)。
1216 字节 → base64 约 1624 字符,确实不好看。但这是后量子的真实价格: ML-KEM 的公钥就是这么大,而客户端必须在**第一个包**里就完成封装才能 0-RTT, 没有"先问服务端要公钥"的余地——那会引入一个 RTT,把机制 1 的全部收益抵消掉。
func ParsePublicKey ¶
ParsePublicKey 解析配置里的 base64 公钥(标准或 URL 变体、有无 padding 都接受)。
func ParsePublicKeyBytes ¶
type Server ¶
type Server struct {
// Handler 处理一条被代理的流。为空时用 DefaultHandler(直连目标)。
Handler func(ctx context.Context, st *Stream)
// PacketHandler 处理一条 UDP 关联。
PacketHandler func(ctx context.Context, ps *PacketStream)
// contains filtered or unexported fields
}
func NewServer ¶
func NewServer(cfg *ServerConfig) (*Server, error)
type ServerConfig ¶
type ServerConfig struct {
// PrivateKey 服务端静态私钥。
PrivateKey *PrivateKey
// Users 允许的用户集合。空表示接受任何通过认证的 user_id
// (只有在 PrivateKey 本身即凭据的部署里才合理,不推荐)。
Users map[[16]byte]string
// TLSConfig 外层 TLS。必须提供(bare 模式与信道绑定都依赖它)。
TLSConfig *tls.Config
// CoverAddr 是掩护源站地址(host:port)。
//
// ★ 认证失败时必须把这条连接的全部字节**真的转发**过去,而不是模拟响应。
// 时序是这里唯一真正难伪造的东西:失败路径 0.1ms、真实站点 50ms,
// 探测方量一下响应时间分布就分开了,伪装随即全部作废。
// 所以掩护站点必须真实可达且延迟合理(同机房或本机)。
CoverAddr string
// TicketStore 为空时用 MemTicketStore(**只在单机正确**,见 TicketStore 文档)。
TicketStore TicketStore
// TicketCount 单批签发数量,0 取 DefaultTicketCount。
TicketCount uint16
// AllowBare 是否允许协商裸帧模式。
AllowBare bool
// SessionGrace 覆盖 DefaultSessionGrace。
SessionGrace time.Duration
// StreamWindow 覆盖 DefaultStreamWindow。
StreamWindow uint64
// MaxStreams 单会话并发流上限。
MaxStreams int
// Congestion 同 ClientConfig.Congestion。
Congestion string
}
ServerConfig 是入站配置。
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session 是由 Session ID 标识的逻辑连接,**独立于任何一条路径存在**。
这一条是整个设计的地基:会话密钥、流状态、身份都不绑定到某条 TCP/QUIC 连接, 于是多路径、无缝迁移、0-RTT 全都从这里派生。反过来说,网络波动下的稳定性 也全都落在这个结构体上——路径来来去去,Session 得一直在。
func (*Session) AcceptStream ¶
AcceptStream 取一条对端开的流(服务端侧)。
func (*Session) KillAllPaths ¶
KillAllPaths 立刻打死本会话的全部路径,不走任何优雅关闭。
这是**故障注入**,模拟"网线被拔了 / Wi-Fi 切走了 / 运营商掐了连接"。 与 Close() 的区别很重要:Close 会发 CLOSE 帧、对端知道该收摊了; 这里对端什么都收不到,只能靠自己的探测与静默计时器发现—— 而那正是真实网络波动的样子,也正是要验的东西。
func (*Session) OpenPacket ¶
OpenPacket 开一条 UDP 关联。dst 是"默认目标",实际每个数据报都自带地址。
func (*Session) OpenStream ¶
OpenStream 开一条流并把目标地址告诉对端。
func (*Session) PathsEstablished ¶
PathsEstablished 返回本会话累计接入过多少条路径。
初始值 1;每一次重连或新增冗余路径 +1。它是判断"恢复逻辑到底跑没跑"的唯一硬证据—— 在环回或低延迟链路上,重连快到几毫秒,光看传输成功与否完全分辨不出来。
type Stream ¶
type Stream struct {
// contains filtered or unexported fields
}
Stream 是会话内的一条应用层字节流,对应一个被代理的 TCP 连接。实现 net.Conn。
★ 为什么这里要重新实现一遍可靠传输(绝对偏移 + 累积 ACK + 重传缓冲), 明明底下的 TCP/QUIC 已经是可靠的了?
因为"可靠"是**每条路径**的属性,而会话要活得比路径长。路径死掉的那一刻, 内核发送队列里已经被 TCP ACK 过、但还没到达对端应用层的字节全部消失, 且本端无从知道丢了哪些——TCP 的 ACK 只保证到达内核,不保证到达对端应用。 没有会话级的偏移与 ACK,一次 Wi-Fi 切换就会让每条流的中间凭空少掉一段字节, 表现为"网页加载到一半卡住"或"下载文件校验和不对",而且不会有任何报错。
代价是发送方要缓存 [ackedOff, sendOff) 这段未确认字节(上限 = 流窗口), 以及每 ackThreshold 字节一个小 ACK 帧。这是无缝迁移的真实价格。
func (*Stream) CloseWrite ¶
CloseWrite 只关半边(对应 TCP 的 shutdown(SHUT_WR))。
func (*Stream) RemoteAddr ¶
type StreamError ¶
type StreamError uint32
StreamError 是 STREAM_RST 携带的错误码。
const ( StreamErrNone StreamError = 0 StreamErrRefused StreamError = 1 // 目标连接失败 StreamErrCanceled StreamError = 2 // 本端主动取消 StreamErrFlowControl StreamError = 3 StreamErrProtocol StreamError = 4 StreamErrSessionClose StreamError = 5 )
func (StreamError) Error ¶
func (e StreamError) Error() string
type TicketStore ¶
type TicketStore interface {
// Issue 为 user 签发一批新票据,返回基址与种子。
Issue(user [16]byte, count uint16) (base uint64, seed [32]byte, err error)
// Consume 原子地消费一张票据。返回 (ticketKey 派生用的种子, ok)。
Consume(user [16]byte, id uint64) (seed [32]byte, ok bool)
// Remaining 返回该用户当前未消费票据数,供补充决策。
Remaining(user [16]byte) int
// Sweep 清理过期批次。
Sweep(now time.Time)
}
TicketStore 是票据消费状态的存储接口。
★ Consume 必须是**原子的**:同一个 (user, id) 并发到达时只能有一个返回 true。 违反这一条,重放保护就完全失效,而且不会有任何报错——攻击者重放的连接会正常建立。 这也是 spec §3.3 步骤 3 要求"置位先于解密 early_data"的原因。
多节点部署:MemTicketStore 只在单机正确。集群必须换成共享实现(Redis 的 SETBIT + Lua 保证原子性,或按 user_id 一致性哈希把同一用户钉在同一节点)。 这是 TIDE 唯一显著增加运维负担的地方。