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:
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
Reference in New Issue
Block a user