feat(udp): add EasyTier analyzer for tcp:// and udp:// transports

Port of NoBrand-Cloud/OpenGFW-RS commit 1177ff7 to Go.

EasyTier's ciphers cover only the payload: the 16-byte PeerManagerHeader
stays in the clear, so peer ids, packet type and two redundant length
fields are readable even on an encrypted mesh. The plaintext handshake
also carries the network name, which lets a rule block one mesh and
leave the rest alone (easytier.network_name). A Syn answered by a Sack
echoing the same connection id and magic, or a handshake carrying the
0xd1e1a5e1 magic, settles a flow on its own; otherwise two consistent
frames are required. Hole-punch probes are reported but never set yes.

The wg://, quic:// and wss:// transports are left to the wireguard, quic
and tls analyzers. fet does not cover the tcp:// transport because it
classifies the plaintext handshake segment and is then done.

Rules must be written as `easytier != nil && ...`: the rule engine only
exposes analyzers that have published properties.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjQ1CTBn8j65cUZ2JKsY7L
This commit is contained in:
2026-09-12 17:05:39 +08:00
co-authored by Claude Fable 5.1
parent f6342c4bcf
commit cc6ad72706
7 changed files with 1577 additions and 3 deletions
+766
View File
@@ -0,0 +1,766 @@
package udp
import (
"encoding/binary"
"maps"
"unicode"
"unicode/utf8"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/utils"
)
// EasyTierAnalyzer 识别 EasyTier(https://github.com/EasyTier/EasyTier)
type EasyTierAnalyzer struct{}
var (
_ analyzer.UDPAnalyzer = (*EasyTierAnalyzer)(nil)
_ analyzer.TCPAnalyzer = (*EasyTierAnalyzer)(nil)
_ analyzer.UDPStream = (*easyTierUDPStream)(nil)
_ analyzer.TCPStream = (*easyTierTCPStream)(nil)
)
const (
// easyTierUDPTunnelHeaderSize UDPTunnelHeader:conn_id u32、msg_type u8、padding u8、len u16,全部小端
easyTierUDPTunnelHeaderSize = 8
// easyTierTCPTunnelHeaderSize TCPTunnelHeader:一个小端 u32 帧体长度
easyTierTCPTunnelHeaderSize = 4
// easyTierPeerManagerHeaderSize PeerManagerHeader:from u32、to u32、type u8、flags u8、fwd u8、reserved u8、len u32
easyTierPeerManagerHeaderSize = 16
// easyTierAEADTailSize AEAD 加密追加的 16 字节 tag 加 12 字节 nonce,头里声明的长度不包含它
easyTierAEADTailSize = 28
// easyTierMaxFrameBody 帧体长度上限。上游自己的限制更低(TCP 隧道 2000,字节流隧道 4096,
// UDP 会话 8192),这里取最大的那个,精确的约束交给长度不变量
easyTierMaxFrameBody = 8192
// easyTierMaxForwardCounter 路由在 >7 时丢弃后再自增,所以线上最大能见到 8
easyTierMaxForwardCounter = 8
// easyTierHandshakeMagic HandshakeRequest.magic,见上游 peers/conn/peer_conn.rs
easyTierHandshakeMagic uint64 = 0xd1e1a5e1
// easyTierSecretDigestLen NetworkSecretDigest = [u8; 32]
easyTierSecretDigestLen = 32
// easyTierNoiseEphemeralLen Noise XX 首条消息以 32 字节临时公钥开头
easyTierNoiseEphemeralLen = 32
// easyTierFramesForYes 没有握手可指的情况下,置 yes 需要攒够的自洽帧数
easyTierFramesForYes = 2
// easyTierUDPInvalidCountThreshold UDP 流上连续无法解析的数据报容忍数,与 wireguard 分析器一致
easyTierUDPInvalidCountThreshold = 4
// easyTierPacketLimit 超过这个包数仍未定性就结束该流,与 openvpn 分析器一致
easyTierPacketLimit = 256
)
// easyTierHolePunchBodyLens 上游打洞包的两种体长:连通性路径 16 字节,UDP 会话层 32 字节。
// 体本身是随机数据,长度是唯一能看的东西。
var easyTierHolePunchBodyLens = [...]int{16, 32}
// UDP 隧道消息类型(UdpPacketType)
const (
easyTierMsgSyn = 1
easyTierMsgSack = 2
easyTierMsgData = 3
easyTierMsgFin = 4
easyTierMsgHolePunch = 5
easyTierMsgV4HolePunch = 6
easyTierMsgV6HolePunch = 7
)
// 需要特殊处理的 PeerManagerHeader packet_type(PacketType)
const (
easyTierPktHandshake = 2
easyTierPktNoiseMsg1 = 13
easyTierPktNoiseMsg3 = 15
)
// PeerManagerHeaderFlags
const (
easyTierFlagEncrypted = 0b0000_0001
easyTierFlagLatencyFirst = 0b0000_0010
easyTierFlagExitNode = 0b0000_0100
easyTierFlagNoProxy = 0b0000_1000
easyTierFlagCompressed = 0b0001_0000
// easyTierFlagLiveness 两个存活探测标志,是仅有的让 reserved 字节有意义的标志
easyTierFlagLiveness = 0b1010_0000
)
func (a *EasyTierAnalyzer) Name() string {
return "easytier"
}
func (a *EasyTierAnalyzer) Limit() int {
// 定长帧协议,按字节截断会让帧读取失步;用 easyTierPacketLimit 约束开销
return 0
}
func (a *EasyTierAnalyzer) NewUDP(info analyzer.UDPInfo, logger analyzer.Logger) analyzer.UDPStream {
return newEasyTierUDPStream(logger)
}
func (a *EasyTierAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
return newEasyTierTCPStream(logger)
}
// easyTierEvidence 一帧能证明多少
type easyTierEvidence int
const (
// easyTierEvidenceWeak 格式正确但单独不足为据:体是随机数据的短定长数据报。字段照发,不推进 yes
easyTierEvidenceWeak easyTierEvidence = iota
// easyTierEvidenceFrame 带 PeerManagerHeader 且通过全部校验的帧
easyTierEvidenceFrame
// easyTierEvidenceConclusive 匹配上的握手,别的东西不会碰巧产生它
easyTierEvidenceConclusive
)
// easyTierCommon 对外发布的属性,加上 yes 背后的计数。
//
// 描述最近一帧的字段每次都会覆盖;从握手里读出来的字段是粘性的,
// 因为网络名只发一次,而按 mesh 名写的规则必须在握手滚过去之后还能用。
type easyTierCommon struct {
props analyzer.PropMap
frames int
conclusive bool
packetCount int
rxPacketCount int
txPacketCount int
}
func newEasyTierCommon(transport string) easyTierCommon {
return easyTierCommon{
props: analyzer.PropMap{
"yes": false,
"transport": transport,
"tx_pkt_cnt": 0,
"rx_pkt_cnt": 0,
},
}
}
func (c *easyTierCommon) countPacket(rev bool) {
c.packetCount++
if rev {
c.rxPacketCount++
c.props["rx_pkt_cnt"] = c.rxPacketCount
} else {
c.txPacketCount++
c.props["tx_pkt_cnt"] = c.txPacketCount
}
}
func (c *easyTierCommon) record(evidence easyTierEvidence) {
switch evidence {
case easyTierEvidenceConclusive:
c.conclusive = true
case easyTierEvidenceFrame:
c.frames++
}
c.props["yes"] = c.conclusive || c.frames >= easyTierFramesForYes
}
func (c *easyTierCommon) set(key string, value interface{}) {
c.props[key] = value
}
// setSticky 只写一次、之后不再覆盖,用于从握手里读出的字段
func (c *easyTierCommon) setSticky(key string, value interface{}) {
if _, ok := c.props[key]; !ok {
c.props[key] = value
}
}
// update 发布一份属性快照。引擎会持有这份 map,所以必须复制,不能把内部的交出去
func (c *easyTierCommon) update() *analyzer.PropUpdate {
return &analyzer.PropUpdate{
Type: analyzer.PropUpdateReplace,
M: maps.Clone(c.props),
}
}
func (c *easyTierCommon) exhausted() bool {
return c.packetCount > easyTierPacketLimit
}
// applyHeader 记录 PeerManagerHeader 的字段,并返回这一帧能证明多少
func (c *easyTierCommon) applyHeader(h easyTierPeerManagerHeader, body []byte) easyTierEvidence {
c.set("packet_type", h.packetType)
c.set("packet_type_name", easyTierPacketTypeName(h.packetType))
c.set("from_peer_id", h.fromPeerID)
c.set("to_peer_id", h.toPeerID)
c.set("forward_counter", h.forwardCounter)
c.set("payload_len", h.length)
c.set("encrypted", h.flags&easyTierFlagEncrypted != 0)
c.set("compressed", h.flags&easyTierFlagCompressed != 0)
c.set("latency_first", h.flags&easyTierFlagLatencyFirst != 0)
c.set("exit_node", h.flags&easyTierFlagExitNode != 0)
c.set("no_proxy", h.flags&easyTierFlagNoProxy != 0)
payload := body[easyTierPeerManagerHeaderSize:]
switch h.packetType {
case easyTierPktHandshake:
return c.applyPlainHandshake(payload)
case easyTierPktNoiseMsg1:
c.setSticky("secure", true)
c.applyNoiseMsg1(payload)
// 只有临时公钥和一段 protobuf,随机流也能凑出来,不算确证
return easyTierEvidenceFrame
case easyTierPktNoiseMsg3:
c.setSticky("secure", true)
return easyTierEvidenceFrame
default:
return easyTierEvidenceFrame
}
}
// applyPlainHandshake 明文 HandshakeRequest,magic 对上就定性
func (c *easyTierCommon) applyPlainHandshake(payload []byte) easyTierEvidence {
hs, ok := easyTierParseHandshakeRequest(payload)
if !ok {
return easyTierEvidenceFrame
}
c.setSticky("magic_ok", true)
c.setSticky("secure", false)
if hs.hasVersion {
c.setSticky("version", hs.version)
}
if hs.networkName != "" {
c.setSticky("network_name", hs.networkName)
}
return easyTierEvidenceConclusive
}
// applyNoiseMsg1 Noise XX 的首条消息此时还没有密钥,protobuf 载荷是明文。
// 机会性地读:解析不干净就什么都不说,不猜。
func (c *easyTierCommon) applyNoiseMsg1(payload []byte) {
if len(payload) <= easyTierNoiseEphemeralLen {
return
}
msg, ok := easyTierParseNoiseMsg1(payload[easyTierNoiseEphemeralLen:])
if !ok {
return
}
if msg.hasVersion {
c.setSticky("version", msg.version)
}
if msg.networkName != "" {
c.setSticky("network_name", msg.networkName)
}
}
type easyTierPeerManagerHeader struct {
fromPeerID uint32
toPeerID uint32
packetType uint8
flags uint8
forwardCounter uint8
length uint32
}
// easyTierReadPeerManagerHeader 读取并校验帧体开头的 PeerManagerHeader。
//
// 声明的载荷长度要和实际收到的帧体对照。AEAD 加密会追加 28 字节尾部而不改声明长度,
// 压缩会缩短载荷也不改声明长度,所以只有两个标志都没置位时关系才是精确相等。
func easyTierReadPeerManagerHeader(body []byte) (easyTierPeerManagerHeader, bool) {
var h easyTierPeerManagerHeader
if len(body) < easyTierPeerManagerHeaderSize {
return h, false
}
packetType := body[8]
if !easyTierIsKnownPacketType(packetType) {
return h, false
}
flags := body[9]
forwardCounter := body[10]
if forwardCounter > easyTierMaxForwardCounter {
return h, false
}
// reserved 只在存活探测时装 token,其余时候恒为 0
if body[11] != 0 && flags&easyTierFlagLiveness == 0 {
return h, false
}
length := binary.LittleEndian.Uint32(body[12:16])
if length > easyTierMaxFrameBody {
return h, false
}
wire := uint32(len(body) - easyTierPeerManagerHeaderSize)
var ok bool
switch {
case flags&easyTierFlagCompressed != 0:
// 压缩器只在能省下自己那 1 字节尾时才压
ok = wire >= 1 && wire <= length+easyTierAEADTailSize
case flags&easyTierFlagEncrypted != 0:
// XOR 不改长度;AEAD 追加尾部
ok = wire == length || wire == length+easyTierAEADTailSize
default:
ok = wire == length
}
if !ok {
return h, false
}
return easyTierPeerManagerHeader{
fromPeerID: binary.LittleEndian.Uint32(body[0:4]),
toPeerID: binary.LittleEndian.Uint32(body[4:8]),
packetType: packetType,
flags: flags,
forwardCounter: forwardCounter,
length: length,
}, true
}
func easyTierIsKnownPacketType(packetType uint8) bool {
return packetType >= 1 && packetType <= 21
}
func easyTierPacketTypeName(packetType uint8) string {
switch packetType {
case 1:
return "data"
case 2:
return "handshake"
case 3:
return "route_packet"
case 4:
return "ping"
case 5:
return "pong"
case 6:
return "ta_rpc"
case 7:
return "route"
case 8:
return "rpc_req"
case 9:
return "rpc_resp"
case 10:
return "foreign_network_packet"
case 11:
return "kcp_src"
case 12:
return "kcp_dst"
case 13:
return "noise_handshake_msg1"
case 14:
return "noise_handshake_msg2"
case 15:
return "noise_handshake_msg3"
case 16:
return "quic_src"
case 17:
return "quic_dst"
case 18:
return "data_with_kcp_src_modified"
case 19:
return "data_with_quic_src_modified"
case 20:
return "relay_handshake"
case 21:
return "relay_handshake_ack"
default:
return "unknown"
}
}
func easyTierMsgTypeName(msgType uint8) string {
switch msgType {
case easyTierMsgSyn:
return "syn"
case easyTierMsgSack:
return "sack"
case easyTierMsgData:
return "data"
case easyTierMsgFin:
return "fin"
case easyTierMsgHolePunch:
return "hole_punch"
case easyTierMsgV4HolePunch:
return "v4_hole_punch"
case easyTierMsgV6HolePunch:
return "v6_hole_punch"
default:
return "unknown"
}
}
// easyTierPendingSyn 一个等着 Sack 的 Syn
type easyTierPendingSyn struct {
connID uint32
magic uint64
rev bool
}
type easyTierUDPStream struct {
logger analyzer.Logger
common easyTierCommon
invalidCount int
pendingSyn *easyTierPendingSyn
}
func newEasyTierUDPStream(logger analyzer.Logger) *easyTierUDPStream {
return &easyTierUDPStream{
logger: logger,
common: newEasyTierCommon("udp"),
}
}
func (s *easyTierUDPStream) Feed(rev bool, data []byte) (u *analyzer.PropUpdate, done bool) {
// EasyTier 把 STUN 复用在同一个 socket 上,并且在自己的帧解析之前先分流 STUN;
// 照做:既不算证据,也不算错误
if easyTierIsSTUNDatagram(data) {
s.common.packetCount++
return nil, s.common.exhausted()
}
evidence, ok := s.parseDatagram(rev, data)
if !ok {
s.invalidCount++
return nil, s.invalidCount >= easyTierUDPInvalidCountThreshold
}
s.invalidCount = 0
s.common.countPacket(rev)
s.common.record(evidence)
return s.common.update(), s.common.exhausted()
}
func (s *easyTierUDPStream) Close(limited bool) *analyzer.PropUpdate {
return nil
}
func (s *easyTierUDPStream) parseDatagram(rev bool, data []byte) (easyTierEvidence, bool) {
if len(data) < easyTierUDPTunnelHeaderSize {
return 0, false
}
connID := binary.LittleEndian.Uint32(data[0:4])
msgType := data[4]
// padding 只为对齐长度字段而存在
if data[5] != 0 {
return 0, false
}
declared := int(binary.LittleEndian.Uint16(data[6:8]))
body := data[easyTierUDPTunnelHeaderSize:]
// 上游要求这两者相等,否则整个数据报直接丢弃
if declared != len(body) {
return 0, false
}
var evidence easyTierEvidence
switch msgType {
case easyTierMsgSyn:
if len(body) != 8 {
return 0, false
}
s.pendingSyn = &easyTierPendingSyn{connID: connID, magic: binary.LittleEndian.Uint64(body), rev: rev}
evidence = easyTierEvidenceWeak
case easyTierMsgSack:
// 反向回来、同一 conn_id、同一随机 64 位 magic 的 Sack,别的东西做不到
if len(body) != 8 {
return 0, false
}
magic := binary.LittleEndian.Uint64(body)
syn := s.pendingSyn
if syn != nil && syn.rev != rev && syn.connID == connID && syn.magic == magic {
evidence = easyTierEvidenceConclusive
} else {
evidence = easyTierEvidenceWeak
}
case easyTierMsgData:
h, ok := easyTierReadPeerManagerHeader(body)
if !ok {
return 0, false
}
evidence = s.common.applyHeader(h, body)
case easyTierMsgFin:
if len(body) != 0 {
return 0, false
}
evidence = easyTierEvidenceWeak
case easyTierMsgHolePunch:
// 打洞探测是定长随机体,往可能根本没跑 EasyTier 的地址上喷,
// 不足以下结论,但 msg_type_name 照样说明它是什么
if !easyTierIsHolePunchBodyLen(len(body)) {
return 0, false
}
evidence = easyTierEvidenceWeak
case easyTierMsgV4HolePunch:
// 两种定向变体在 conn_id 里回显目标端口。上游只从回环地址接受它们,主机间线上少见
if len(body) != 6 || connID != uint32(binary.LittleEndian.Uint16(body[4:6])) {
return 0, false
}
evidence = easyTierEvidenceWeak
case easyTierMsgV6HolePunch:
if len(body) != 38 || connID != uint32(binary.LittleEndian.Uint16(body[16:18])) {
return 0, false
}
evidence = easyTierEvidenceWeak
default:
return 0, false
}
s.common.set("conn_id", connID)
s.common.set("msg_type", msgType)
s.common.set("msg_type_name", easyTierMsgTypeName(msgType))
return evidence, true
}
func easyTierIsHolePunchBodyLen(n int) bool {
for _, l := range easyTierHolePunchBodyLens {
if n == l {
return true
}
}
return false
}
// easyTierIsSTUNDatagram 和 EasyTier 自己先做的判断一样:
// 它的长度字段位置上是 STUN magic cookie,且前两位为零
func easyTierIsSTUNDatagram(data []byte) bool {
return len(data) >= easyTierUDPTunnelHeaderSize &&
data[4] == 0x21 && data[5] == 0x12 && data[6] == 0xa4 && data[7] == 0x42 &&
data[0]&0xc0 == 0
}
type easyTierTCPStream struct {
logger analyzer.Logger
common easyTierCommon
reqBuf utils.ByteBuffer
respBuf utils.ByteBuffer
}
func newEasyTierTCPStream(logger analyzer.Logger) *easyTierTCPStream {
return &easyTierTCPStream{
logger: logger,
common: newEasyTierCommon("tcp"),
}
}
// easyTierFrameStatus 从一个方向的缓冲里读一帧的结果
type easyTierFrameStatus int
const (
// easyTierFrameRead 头已校验通过,帧体可以发布
easyTierFrameRead easyTierFrameStatus = iota
// easyTierFrameNeedMore 声明的帧体还没到齐
easyTierFrameNeedMore
// easyTierFrameNo 不可能是 EasyTier 的帧
easyTierFrameNo
)
func (s *easyTierTCPStream) Feed(rev, start, end bool, skip int, data []byte) (u *analyzer.PropUpdate, done bool) {
// 重组有空洞时帧边界无从推断
if skip != 0 {
return nil, true
}
if len(data) == 0 {
return nil, false
}
buf := &s.reqBuf
if rev {
buf = &s.respBuf
}
buf.Append(data)
updated := false
for {
h, body, status := easyTierReadFrame(buf)
switch status {
case easyTierFrameRead:
evidence := s.common.applyHeader(h, body)
s.common.countPacket(rev)
s.common.record(evidence)
updated = true
if s.common.exhausted() {
return s.common.update(), true
}
case easyTierFrameNeedMore:
if updated {
return s.common.update(), false
}
return nil, false
case easyTierFrameNo:
if updated {
return s.common.update(), true
}
return nil, true
}
}
}
func (s *easyTierTCPStream) Close(limited bool) *analyzer.PropUpdate {
s.reqBuf.Reset()
s.respBuf.Reset()
return nil
}
// easyTierReadFrame 帧结构是 [u32 LE 帧体长度][帧体],帧体总是以 PeerManagerHeader 开头,
// 上游会把更短的帧体按 "body too short" 拒绝。
func easyTierReadFrame(buf *utils.ByteBuffer) (easyTierPeerManagerHeader, []byte, easyTierFrameStatus) {
var h easyTierPeerManagerHeader
bodyLen, ok := buf.GetUint32(true, false)
if !ok {
return h, nil, easyTierFrameNeedMore
}
if bodyLen < easyTierPeerManagerHeaderSize || bodyLen > easyTierMaxFrameBody {
return h, nil, easyTierFrameNo
}
total := easyTierTCPTunnelHeaderSize + int(bodyLen)
frame, ok := buf.Get(total, false)
if !ok {
return h, nil, easyTierFrameNeedMore
}
body := frame[easyTierTCPTunnelHeaderSize:]
h, ok = easyTierReadPeerManagerHeader(body)
if !ok {
return h, nil, easyTierFrameNo
}
// 只在帧确认合法后才消费;被拒绝的流反正要结束,留着字节让缓冲状态更好推理
buf.Skip(total)
return h, body, easyTierFrameRead
}
// easyTierHandshakeInfo 从握手里读出来的东西。networkName 为空表示没有可用的名字
type easyTierHandshakeInfo struct {
version uint64
hasVersion bool
networkName string
}
// easyTierParseHandshakeRequest 解析 HandshakeRequest { magic = 1, my_peer_id = 2,
// version = 3, features = 4, network_name = 5, network_secret_digest = 6 }。
//
// 只有 magic 存在且正确才返回 true,调用方可以把 true 当作证据而不是提示。
func easyTierParseHandshakeRequest(payload []byte) (easyTierHandshakeInfo, bool) {
var info easyTierHandshakeInfo
var magic uint64
hasMagic := false
digestLen := -1
ok := easyTierForEachProtoField(payload, func(f easyTierProtoField) {
switch {
case f.isVarint && f.number == 1:
magic, hasMagic = f.varint, true
case f.isVarint && f.number == 3:
info.version, info.hasVersion = f.varint, true
case f.isBytes && f.number == 5:
info.networkName, _ = easyTierPrintableUTF8(f.bytes)
case f.isBytes && f.number == 6:
digestLen = len(f.bytes)
}
})
if !ok || !hasMagic || magic != easyTierHandshakeMagic {
return easyTierHandshakeInfo{}, false
}
// 上游会拒绝 digest 宽度不对的握手
if digestLen >= 0 && digestLen != easyTierSecretDigestLen {
return easyTierHandshakeInfo{}, false
}
return info, true
}
// easyTierParseNoiseMsg1 解析 PeerConnNoiseMsg1Pb { version = 1, a_network_name = 2, ... }。
//
// 这里没有 magic 可查,所以消息必须完整解析并声明一个合理的版本,否则返回 false。
func easyTierParseNoiseMsg1(payload []byte) (easyTierHandshakeInfo, bool) {
var info easyTierHandshakeInfo
ok := easyTierForEachProtoField(payload, func(f easyTierProtoField) {
switch {
case f.isVarint && f.number == 1:
info.version, info.hasVersion = f.varint, true
case f.isBytes && f.number == 2:
info.networkName, _ = easyTierPrintableUTF8(f.bytes)
}
})
// 协议版本从有这个字段起就一直是 1,很大的值说明这些字节不是这条消息
if !ok || !info.hasVersion || info.version > 16 {
return easyTierHandshakeInfo{}, false
}
return info, true
}
type easyTierProtoField struct {
number uint32
isVarint bool
varint uint64
isBytes bool
bytes []byte
}
// easyTierForEachProtoField 遍历 protobuf 字段,跳过调用方不认识的字段,
// 遇到编码站不住脚(截断、字段号为 0、这两条消息从不使用的 wire type)时返回 false。
func easyTierForEachProtoField(data []byte, fn func(easyTierProtoField)) bool {
pos := 0
for pos < len(data) {
tag, ok := easyTierReadVarint(data, &pos)
if !ok {
return false
}
number := uint32(tag >> 3)
if number == 0 {
return false
}
switch tag & 0x7 {
case 0:
value, ok := easyTierReadVarint(data, &pos)
if !ok {
return false
}
fn(easyTierProtoField{number: number, isVarint: true, varint: value})
case 2:
length, ok := easyTierReadVarint(data, &pos)
if !ok || length > uint64(len(data)-pos) {
return false
}
end := pos + int(length)
fn(easyTierProtoField{number: number, isBytes: true, bytes: data[pos:end]})
pos = end
case 5:
if len(data)-pos < 4 {
return false
}
pos += 4
case 1:
if len(data)-pos < 8 {
return false
}
pos += 8
default:
// group 类型,两条消息都不用
return false
}
}
return true
}
func easyTierReadVarint(data []byte, pos *int) (uint64, bool) {
var value uint64
for i := 0; i < 10; i++ {
if *pos >= len(data) {
return 0, false
}
b := data[*pos]
*pos++
value |= uint64(b&0x7f) << (7 * i)
if b&0x80 == 0 {
return value, true
}
}
return 0, false
}
// easyTierPrintableUTF8 网络名是运营者自己起的文本。拒绝控制字符,
// 免得一段碰巧的字节最后被引用进日志或审计记录里。
func easyTierPrintableUTF8(b []byte) (string, bool) {
if len(b) == 0 || !utf8.Valid(b) {
return "", false
}
s := string(b)
for _, r := range s {
if unicode.IsControl(r) {
return "", false
}
}
return s, true
}
+562
View File
@@ -0,0 +1,562 @@
package udp
import (
"bytes"
"encoding/binary"
"fmt"
"testing"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/tcp"
)
func etVarint(v uint64) []byte {
var out []byte
for {
b := byte(v & 0x7f)
v >>= 7
if v != 0 {
b |= 0x80
}
out = append(out, b)
if v == 0 {
return out
}
}
}
func etProtoVarint(field uint32, v uint64) []byte {
return append(etVarint(uint64(field)<<3), etVarint(v)...)
}
func etProtoBytes(field uint32, v []byte) []byte {
out := etVarint(uint64(field)<<3 | 2)
out = append(out, etVarint(uint64(len(v)))...)
return append(out, v...)
}
// etHandshakeRequest HandshakeRequest,按 send_handshake 的构造方式
func etHandshakeRequest(peerID uint32, networkName string) []byte {
var out []byte
out = append(out, etProtoVarint(1, easyTierHandshakeMagic)...)
out = append(out, etProtoVarint(2, uint64(peerID))...)
out = append(out, etProtoVarint(3, 1)...)
out = append(out, etProtoBytes(4, []byte("liveness-echo-v1"))...)
out = append(out, etProtoBytes(5, []byte(networkName))...)
out = append(out, etProtoBytes(6, make([]byte, easyTierSecretDigestLen))...)
return out
}
// etNoiseMsg1 Noise XX 先写的临时公钥,后面跟 PeerConnNoiseMsg1Pb
func etNoiseMsg1(networkName string) []byte {
out := bytes.Repeat([]byte{0xab}, easyTierNoiseEphemeralLen)
out = append(out, etProtoVarint(1, 1)...)
return append(out, etProtoBytes(2, []byte(networkName))...)
}
// etBody 帧体:16 字节 PeerManagerHeader 加载荷
func etBody(packetType uint8, from, to uint32, payload []byte) []byte {
return etBodyRaw(packetType, from, to, 0, 1, 0, uint32(len(payload)), payload)
}
func etBodyRaw(packetType uint8, from, to uint32, flags, forwardCounter, reserved uint8, declaredLen uint32, payload []byte) []byte {
out := make([]byte, 0, easyTierPeerManagerHeaderSize+len(payload))
out = binary.LittleEndian.AppendUint32(out, from)
out = binary.LittleEndian.AppendUint32(out, to)
out = append(out, packetType, flags, forwardCounter, reserved)
out = binary.LittleEndian.AppendUint32(out, declaredLen)
return append(out, payload...)
}
// etDatagram 8 字节 UDPTunnelHeader 加帧体
func etDatagram(connID uint32, msgType uint8, body []byte) []byte {
out := make([]byte, 0, easyTierUDPTunnelHeaderSize+len(body))
out = binary.LittleEndian.AppendUint32(out, connID)
out = append(out, msgType, 0)
out = binary.LittleEndian.AppendUint16(out, uint16(len(body)))
return append(out, body...)
}
func etSyn(connID uint32, magic uint64) []byte {
return etDatagram(connID, easyTierMsgSyn, binary.LittleEndian.AppendUint64(nil, magic))
}
func etSack(connID uint32, magic uint64) []byte {
return etDatagram(connID, easyTierMsgSack, binary.LittleEndian.AppendUint64(nil, magic))
}
// etSegment 4 字节 TCPTunnelHeader 加帧体
func etSegment(body []byte) []byte {
out := binary.LittleEndian.AppendUint32(nil, uint32(len(body)))
return append(out, body...)
}
func etUDP() analyzer.UDPStream {
return (&EasyTierAnalyzer{}).NewUDP(analyzer.UDPInfo{}, nil)
}
func etTCP() analyzer.TCPStream {
return (&EasyTierAnalyzer{}).NewTCP(analyzer.TCPInfo{}, nil)
}
func etFeedUDP(s analyzer.UDPStream, rev bool, data []byte) (analyzer.PropMap, bool) {
u, done := s.Feed(rev, data)
if u == nil {
return analyzer.PropMap{}, done
}
return u.M, done
}
func etFeedTCP(s analyzer.TCPStream, rev bool, data []byte) (analyzer.PropMap, bool) {
u, done := s.Feed(rev, false, false, 0, data)
if u == nil {
return analyzer.PropMap{}, done
}
return u.M, done
}
func etText(m analyzer.PropMap, key string) string {
v, ok := m[key]
if !ok {
return "<missing>"
}
return fmt.Sprint(v)
}
func etFlag(m analyzer.PropMap, key string) bool {
v, _ := m[key].(bool)
return v
}
func etExpect(t *testing.T, m analyzer.PropMap, key, want string) {
t.Helper()
if got := etText(m, key); got != want {
t.Fatalf("%s = %q, want %q (props: %v)", key, got, want, m)
}
}
func TestEasyTier_HandshakeMagicEncodesToDocumentedBytes(t *testing.T) {
want := []byte{0x08, 0xe1, 0xcb, 0x86, 0x8f, 0x0d}
if got := etProtoVarint(1, easyTierHandshakeMagic); !bytes.Equal(got, want) {
t.Fatalf("magic encodes to % x, want % x", got, want)
}
}
func TestEasyTier_UDPHandshakeNamesTheMeshOnFirstDatagram(t *testing.T) {
payload := etHandshakeRequest(0x1a2b3c4d, "office-mesh")
m, done := etFeedUDP(etUDP(), false, etDatagram(7, easyTierMsgData, etBody(easyTierPktHandshake, 0x1a2b3c4d, 0, payload)))
if done {
t.Fatal("stream done after a single handshake")
}
if !etFlag(m, "yes") {
t.Fatal("the magic alone should settle it")
}
if !etFlag(m, "magic_ok") {
t.Fatal("magic_ok not set")
}
etExpect(t, m, "network_name", "office-mesh")
etExpect(t, m, "version", "1")
etExpect(t, m, "transport", "udp")
etExpect(t, m, "msg_type_name", "data")
etExpect(t, m, "packet_type_name", "handshake")
etExpect(t, m, "from_peer_id", "439041101")
etExpect(t, m, "to_peer_id", "0")
etExpect(t, m, "secure", "false")
}
func TestEasyTier_TCPHandshakeNamesTheMeshOnFirstFrame(t *testing.T) {
payload := etHandshakeRequest(9, "office-mesh")
m, done := etFeedTCP(etTCP(), false, etSegment(etBody(easyTierPktHandshake, 9, 0, payload)))
if done {
t.Fatal("stream done after a single handshake")
}
if !etFlag(m, "yes") {
t.Fatal("yes not set")
}
etExpect(t, m, "network_name", "office-mesh")
etExpect(t, m, "transport", "tcp")
etExpect(t, m, "tx_pkt_cnt", "1")
}
func TestEasyTier_HandshakeWithWrongMagicIsNotConclusive(t *testing.T) {
payload := append(etProtoVarint(1, 0xd1e1a5e0), etProtoBytes(5, []byte("office-mesh"))...)
m, _ := etFeedUDP(etUDP(), false, etDatagram(7, easyTierMsgData, etBody(easyTierPktHandshake, 1, 0, payload)))
// 帧结构仍然自洽,字段照发;但一帧不够,而且对网络不做任何声明
if etFlag(m, "yes") {
t.Fatal("yes set on a wrong magic")
}
etExpect(t, m, "network_name", "<missing>")
etExpect(t, m, "magic_ok", "<missing>")
}
func TestEasyTier_HandshakeWithWrongDigestWidthIsRejected(t *testing.T) {
// 上游这种情况会拒连,报匹配等于报一个根本连不起来的 mesh 名
payload := etProtoVarint(1, easyTierHandshakeMagic)
payload = append(payload, etProtoBytes(5, []byte("office-mesh"))...)
payload = append(payload, etProtoBytes(6, make([]byte, 16))...)
m, _ := etFeedUDP(etUDP(), false, etDatagram(7, easyTierMsgData, etBody(easyTierPktHandshake, 1, 0, payload)))
if etFlag(m, "yes") {
t.Fatal("yes set on a wrong digest width")
}
etExpect(t, m, "network_name", "<missing>")
}
func TestEasyTier_NoiseFirstMessageStillGivesUpTheNetworkName(t *testing.T) {
// Noise XX 写 msg1 时还没有密钥,临时公钥后面的载荷是明文
s := etUDP()
m, _ := etFeedUDP(s, false, etDatagram(7, easyTierMsgData, etBody(easyTierPktNoiseMsg1, 5, 0, etNoiseMsg1("secure-mesh"))))
if !etFlag(m, "secure") {
t.Fatal("secure not set")
}
etExpect(t, m, "network_name", "secure-mesh")
if etFlag(m, "yes") {
t.Fatal("one frame with no magic to check should not settle it")
}
// 反方向再来一帧就到阈值了
m, _ = etFeedUDP(s, true, etDatagram(7, easyTierMsgData, etBody(14, 6, 5, bytes.Repeat([]byte{0xcd}, 48))))
if !etFlag(m, "yes") {
t.Fatal("two consistent frames should settle it")
}
etExpect(t, m, "packet_type_name", "noise_handshake_msg2")
}
func TestEasyTier_OpaqueNoiseMessageSaysNothingAboutTheNetwork(t *testing.T) {
m, _ := etFeedUDP(etUDP(), false, etDatagram(7, easyTierMsgData, etBody(easyTierPktNoiseMsg1, 5, 0, bytes.Repeat([]byte{0x5a}, 96))))
if !etFlag(m, "secure") {
t.Fatal("secure not set")
}
etExpect(t, m, "network_name", "<missing>")
}
func TestEasyTier_SynAnsweredByItsSackSettlesTheFlow(t *testing.T) {
s := etUDP()
m, _ := etFeedUDP(s, false, etSyn(0x12345678, 0x0102030405060708))
if etFlag(m, "yes") {
t.Fatal("a lone syn is 16 bytes of nearly nothing")
}
etExpect(t, m, "msg_type_name", "syn")
m, _ = etFeedUDP(s, true, etSack(0x12345678, 0x0102030405060708))
if !etFlag(m, "yes") {
t.Fatal("a matching sack should settle it")
}
etExpect(t, m, "msg_type_name", "sack")
etExpect(t, m, "conn_id", "305419896")
etExpect(t, m, "tx_pkt_cnt", "1")
etExpect(t, m, "rx_pkt_cnt", "1")
}
func TestEasyTier_SackEchoingTheWrongMagicSettlesNothing(t *testing.T) {
s := etUDP()
etFeedUDP(s, false, etSyn(0x12345678, 1))
m, _ := etFeedUDP(s, true, etSack(0x12345678, 2))
if etFlag(m, "yes") {
t.Fatal("yes set on a mismatched magic")
}
}
func TestEasyTier_SackDownTheSameDirectionSettlesNothing(t *testing.T) {
s := etUDP()
etFeedUDP(s, false, etSyn(9, 42))
m, _ := etFeedUDP(s, false, etSack(9, 42))
if etFlag(m, "yes") {
t.Fatal("yes set on a same-direction sack")
}
}
func TestEasyTier_HolePunchProbesAreReportedButDoNotSettleTheFlow(t *testing.T) {
// 往可能根本没跑 EasyTier 的地址上喷的,规则得点名才拦
s := etUDP()
var m analyzer.PropMap
for i := 0; i < 3; i++ {
m, _ = etFeedUDP(s, false, etDatagram(1, easyTierMsgHolePunch, bytes.Repeat([]byte{0x7f}, 16)))
}
if etFlag(m, "yes") {
t.Fatal("hole punches settled the flow")
}
etExpect(t, m, "msg_type_name", "hole_punch")
}
func TestEasyTier_BothHolePunchBodyLengthsAreRecognised(t *testing.T) {
// 连通性路径 16 字节,UDP 会话层 32 字节;只有长度能把它们和噪声分开,两个都得列
for _, n := range []int{16, 32} {
m, _ := etFeedUDP(etUDP(), false, etDatagram(1, easyTierMsgHolePunch, bytes.Repeat([]byte{0x7f}, n)))
if got := etText(m, "msg_type_name"); got != "hole_punch" {
t.Fatalf("body length %d: msg_type_name = %q", n, got)
}
}
m, _ := etFeedUDP(etUDP(), false, etDatagram(1, easyTierMsgHolePunch, bytes.Repeat([]byte{0x7f}, 24)))
etExpect(t, m, "msg_type_name", "<missing>")
}
func TestEasyTier_DirectedHolePunchMustEchoTheDestinationPortInConnID(t *testing.T) {
punch := []byte{203, 0, 113, 9}
punch = binary.LittleEndian.AppendUint16(punch, 11010)
m, _ := etFeedUDP(etUDP(), false, etDatagram(11010, easyTierMsgV4HolePunch, punch))
etExpect(t, m, "msg_type_name", "v4_hole_punch")
m, done := etFeedUDP(etUDP(), false, etDatagram(11011, easyTierMsgV4HolePunch, punch))
etExpect(t, m, "msg_type_name", "<missing>")
if done {
t.Fatal("one bad datagram is not enough to give up")
}
}
func TestEasyTier_UDPLengthFieldMustMatchTheDatagram(t *testing.T) {
wrong := etSyn(1, 2)
wrong[6] = 9 // 声明 9,体是 8
m, _ := etFeedUDP(etUDP(), false, wrong)
etExpect(t, m, "msg_type_name", "<missing>")
}
func TestEasyTier_UDPPaddingByteMustBeZero(t *testing.T) {
wrong := etSyn(1, 2)
wrong[5] = 1
m, _ := etFeedUDP(etUDP(), false, wrong)
etExpect(t, m, "msg_type_name", "<missing>")
}
func TestEasyTier_FourUnparseableDatagramsEndAUDPFlow(t *testing.T) {
s := etUDP()
for i := 0; i < 3; i++ {
if _, done := etFeedUDP(s, false, []byte("not easytier at all")); done {
t.Fatalf("gave up after %d datagrams", i+1)
}
}
if _, done := etFeedUDP(s, false, []byte("not easytier at all")); !done {
t.Fatal("did not give up after four unparseable datagrams")
}
}
func TestEasyTier_TwoLengthFieldsOfATCPFrameMustAgree(t *testing.T) {
// 16 字节头后面声明载荷 4,实际到了 8
frame := etBodyRaw(1, 1, 2, 0, 1, 0, 4, bytes.Repeat([]byte{0xaa}, 8))
m, done := etFeedTCP(etTCP(), false, etSegment(frame))
if !done {
t.Fatal("stream not done on a length mismatch")
}
etExpect(t, m, "packet_type_name", "<missing>")
}
func TestEasyTier_AEADTailIsAllowedToOutrunTheDeclaredLength(t *testing.T) {
// 加密器追加 16 字节 tag 和 12 字节 nonce,不改头里声明的长度
payload := bytes.Repeat([]byte{0x11}, 40+easyTierAEADTailSize)
frame := etBodyRaw(1, 1, 2, easyTierFlagEncrypted, 1, 0, 40, payload)
m, _ := etFeedTCP(etTCP(), false, etSegment(frame))
if !etFlag(m, "encrypted") {
t.Fatal("encrypted not set")
}
etExpect(t, m, "payload_len", "40")
}
func TestEasyTier_CompressedPayloadMayComeInUnderTheDeclaredLength(t *testing.T) {
frame := etBodyRaw(1, 1, 2, easyTierFlagCompressed, 1, 0, 900, bytes.Repeat([]byte{0x22}, 61))
m, _ := etFeedTCP(etTCP(), false, etSegment(frame))
if !etFlag(m, "compressed") {
t.Fatal("compressed not set")
}
etExpect(t, m, "payload_len", "900")
}
func TestEasyTier_UnencryptedPayloadMustMatchTheDeclaredLengthExactly(t *testing.T) {
frame := etBodyRaw(1, 1, 2, 0, 1, 0, 40, bytes.Repeat([]byte{0x11}, 40+easyTierAEADTailSize))
if _, done := etFeedTCP(etTCP(), false, etSegment(frame)); !done {
t.Fatal("stream not done on an unencrypted length mismatch")
}
}
func TestEasyTier_ReservedByteIsZeroUnlessALivenessFlagIsSet(t *testing.T) {
withFlag := etBodyRaw(1, 1, 2, 0b0010_0000, 1, 0x5c, 4, make([]byte, 4))
m, _ := etFeedTCP(etTCP(), false, etSegment(withFlag))
etExpect(t, m, "packet_type_name", "data")
withoutFlag := etBodyRaw(1, 1, 2, 0, 1, 0x5c, 4, make([]byte, 4))
m, done := etFeedTCP(etTCP(), false, etSegment(withoutFlag))
if !done {
t.Fatal("stream not done on a non-zero reserved byte")
}
etExpect(t, m, "packet_type_name", "<missing>")
}
func TestEasyTier_ForwardCounterPastTheRoutersOwnCapIsRejected(t *testing.T) {
frame := etBodyRaw(1, 1, 2, 0, 9, 0, 4, make([]byte, 4))
m, done := etFeedTCP(etTCP(), false, etSegment(frame))
if !done {
t.Fatal("stream not done on forward counter 9")
}
etExpect(t, m, "packet_type_name", "<missing>")
}
func TestEasyTier_UnnamedPacketTypeIsRejected(t *testing.T) {
frame := etBodyRaw(22, 1, 2, 0, 1, 0, 4, make([]byte, 4))
if _, done := etFeedTCP(etTCP(), false, etSegment(frame)); !done {
t.Fatal("stream not done on packet type 22")
}
}
func TestEasyTier_FrameSplitAcrossSegmentsIsReassembled(t *testing.T) {
wire := etSegment(etBody(easyTierPktHandshake, 9, 0, etHandshakeRequest(9, "office-mesh")))
s := etTCP()
m, done := etFeedTCP(s, false, wire[:3])
if done {
t.Fatal("gave up on a length field that had not fully arrived")
}
etExpect(t, m, "packet_type_name", "<missing>")
m, done = etFeedTCP(s, false, wire[3:])
if done {
t.Fatal("stream done after a valid handshake")
}
if !etFlag(m, "yes") {
t.Fatal("yes not set after reassembly")
}
etExpect(t, m, "network_name", "office-mesh")
}
func TestEasyTier_ByteAtATimeDeliveryStillReadsTheFrame(t *testing.T) {
wire := etSegment(etBody(easyTierPktHandshake, 9, 0, etHandshakeRequest(9, "mesh")))
s := etTCP()
last := analyzer.PropMap{}
for _, b := range wire {
m, done := etFeedTCP(s, false, []byte{b})
if done {
t.Fatal("stream done mid-frame")
}
if len(m) != 0 {
last = m
}
}
if !etFlag(last, "yes") {
t.Fatal("yes not set after byte-at-a-time delivery")
}
etExpect(t, last, "network_name", "mesh")
}
func TestEasyTier_SeveralFramesPipelinedIntoOneSegmentAreAllRead(t *testing.T) {
wire := etSegment(etBody(1, 9, 8, bytes.Repeat([]byte{0x01}, 32)))
wire = append(wire, etSegment(etBody(4, 9, 8, nil))...)
wire = append(wire, etSegment(etBody(1, 9, 8, bytes.Repeat([]byte{0x03}, 64)))...)
m, done := etFeedTCP(etTCP(), false, wire)
if done {
t.Fatal("stream done after valid frames")
}
if !etFlag(m, "yes") {
t.Fatal("yes not set after three consistent frames")
}
etExpect(t, m, "tx_pkt_cnt", "3")
// map 描述的是最新一帧
etExpect(t, m, "payload_len", "64")
}
func TestEasyTier_NetworkNameSurvivesLaterFrames(t *testing.T) {
// 只发一次。按 mesh 名写的规则得在握手滚过去之后还能用
s := etTCP()
etFeedTCP(s, false, etSegment(etBody(easyTierPktHandshake, 9, 0, etHandshakeRequest(9, "office-mesh"))))
m, _ := etFeedTCP(s, false, etSegment(etBody(1, 9, 8, bytes.Repeat([]byte{0xee}, 128))))
etExpect(t, m, "network_name", "office-mesh")
etExpect(t, m, "packet_type_name", "data")
}
func TestEasyTier_GapInTheTCPStreamEndsIt(t *testing.T) {
u, done := etTCP().Feed(false, false, false, 7, []byte("whatever"))
if u != nil {
t.Fatal("update published on a gap")
}
if !done {
t.Fatal("stream not done on a gap")
}
}
func TestEasyTier_OrdinaryTrafficIsNotMistakenForATunnel(t *testing.T) {
m, done := etFeedTCP(etTCP(), false, []byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"))
if !done || etFlag(m, "yes") {
t.Fatalf("HTTP: done=%v yes=%v", done, etFlag(m, "yes"))
}
// TLS ClientHello 记录:16 03 01 <len> ... 读出来是 21 MB 的帧体长度,直接拒绝
m, done = etFeedTCP(etTCP(), false, []byte{0x16, 0x03, 0x01, 0x01, 0x4a, 0x01, 0x00, 0x01, 0x46})
if !done || etFlag(m, "yes") {
t.Fatalf("TLS: done=%v yes=%v", done, etFlag(m, "yes"))
}
}
func TestEasyTier_FETDoesNotCatchTheTCPHandshake(t *testing.T) {
// fet 只看客户端第一个分段,判完就 done。EasyTier 连接的第一个分段永远是明文握手,
// 整条连接上唯一低熵的东西,所以 fet 读作"非全加密",引擎放行,后面真正加密的帧再也不看。
// 而且能不能中取决于握手里碰巧有多少随机内容(ex1 落在 3.4 豁免线附近,随网络名长短摇摆),
// 一个随运营者起的名字长短而变的检测器不是检测器。
digest := make([]byte, 32)
for i := range digest {
digest[i] = byte(uint32(i) * 2654435761)
}
payload := etProtoVarint(1, easyTierHandshakeMagic)
payload = append(payload, etProtoVarint(2, 0x1a2b3c4d)...)
payload = append(payload, etProtoVarint(3, 1)...)
payload = append(payload, etProtoBytes(4, []byte("liveness-echo-v1"))...)
payload = append(payload, etProtoBytes(5, []byte("mesh"))...)
payload = append(payload, etProtoBytes(6, digest)...)
wire := etSegment(etBody(easyTierPktHandshake, 0x1a2b3c4d, 0, payload))
fet := (&tcp.FETAnalyzer{}).NewTCP(analyzer.TCPInfo{}, nil)
u, done := fet.Feed(false, true, false, 0, wire)
if !done {
t.Fatal("fet should decide on the first segment and not look again")
}
if u != nil && etFlag(u.M, "yes") {
t.Fatal("fet claimed this handshake, so this analyzer would be redundant")
}
// 同样的字节,给本分析器
m, _ := etFeedTCP(etTCP(), false, wire)
if !etFlag(m, "yes") {
t.Fatal("easytier did not claim its own handshake")
}
etExpect(t, m, "network_name", "mesh")
}
func TestEasyTier_WireGuardAnalyzerDoesNotClaimUDPTransport(t *testing.T) {
// wireguard 覆盖的是 EasyTier 的 wg:// 监听,六分之一。udp:// 上没有任何东西读起来像 WireGuard 报文
datagrams := [][]byte{
etSyn(0x12345678, 0x0102030405060708),
etDatagram(7, easyTierMsgData, etBody(easyTierPktHandshake, 0x1a2b3c4d, 0, etHandshakeRequest(9, "mesh"))),
etDatagram(1, easyTierMsgHolePunch, bytes.Repeat([]byte{0x7f}, 16)),
}
for i, data := range datagrams {
wg := (&WireGuardAnalyzer{}).NewUDP(analyzer.UDPInfo{}, nil)
if u, _ := wg.Feed(false, data); u != nil {
t.Fatalf("wireguard claimed easytier datagram %d: %v", i, u.M)
}
}
}
func TestEasyTier_STUNOnTheSameSocketIsNeitherEvidenceNorHeldAgainstTheFlow(t *testing.T) {
// EasyTier 把 STUN 复用在自己的 UDP socket 上,并在自己的帧解析之前分流;四个 STUN 不能结束流
stun := append([]byte{0x00, 0x01, 0x00, 0x00, 0x21, 0x12, 0xa4, 0x42}, bytes.Repeat([]byte{0x11}, 12)...)
s := etUDP()
for i := 0; i < 8; i++ {
m, done := etFeedUDP(s, false, stun)
if done {
t.Fatalf("STUN datagram %d ended the flow", i+1)
}
if len(m) != 0 {
t.Fatalf("STUN datagram %d published props: %v", i+1, m)
}
}
m, _ := etFeedUDP(s, false, etSyn(1, 2))
etExpect(t, m, "msg_type_name", "syn")
}