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 }