feat: crack all DMX/RDM frame fields; show mode now fully self-constructed
- analyze subcommand: offline replay/template field variance, diff, counter analysis - DMX header fully decoded: port mask @0x20, port lens u16LE=512 @0x34/0x36, counter +1/frame - RDM request frame fully decoded: counter=0, 0x20=02, 0x2A=0xCC, msgLen=24+PDL, checksum=0xCC+sum(0x3C..0x53+PDL) @0x53+PDL, pair copy rules - Fix historical misreadings: RDM always starts @0x3C (not variable); no session-level frame check exists - earlier failures were wrong template + checksum off-by-one - show mode: build_dmx_frame/build_rdm_frame2/rdm_pair_copy, zero template dependency - Decode 47B register response and 42B event frames; doc enet_protocol_current.md rewritten
This commit is contained in:
parent
9995335d74
commit
9be449fda8
|
|
@ -66,9 +66,9 @@ $env:HOSTDEMO_MODE = "show"
|
||||||
.\target\release\enet-validator.exe hostdemo 192.168.123.1 600
|
.\target\release\enet-validator.exe hostdemo 192.168.123.1 600
|
||||||
```
|
```
|
||||||
|
|
||||||
- DMX:地址 1-32 彩虹呼吸渐变(HMKJ 32 通道灯具)
|
- DMX:地址 1-32 彩虹呼吸渐变(HMKJ 32 通道灯具),**零模板纯自构造帧**(`build_dmx_frame`)
|
||||||
- RDM:每 700ms 轮询 3 台已知 UID,实时打印 `DEVICE_INFO` 与 `DMX_START_ADDRESS`
|
- RDM:每 700ms 轮询 3 台已知 UID,实时打印 `DEVICE_INFO` 与 `DMX_START_ADDRESS`,**纯自构造请求帧**(`build_rdm_frame2`)
|
||||||
- 依赖:仅需首次抓包文件 `replay_frames.txt` 作为帧骨架模板(程序自带 `samples/` 中亦有备用模板)
|
- 运行无需任何抓包/replay 文件;DMX/RDM 帧全字段已破解(见 docs/enet_protocol_current.md)
|
||||||
|
|
||||||
## 已破解要点(详见 docs/enet_protocol.md)
|
## 已破解要点(详见 docs/enet_protocol.md)
|
||||||
|
|
||||||
|
|
|
||||||
149
docs/enet_protocol_current.md
Normal file
149
docs/enet_protocol_current.md
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
# e:net 协议当前还原状态(截至 2026-09-11,全面逆向后)
|
||||||
|
|
||||||
|
本文档汇总基于抓包与当前程序实现的协议还原状态。
|
||||||
|
**2026-09-11 全面逆向结论:DMX 帧与 RDM 请求帧已 100% 字段破解,可零模板纯自构造。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、UDP/31772 通用信封(全部帧)
|
||||||
|
|
||||||
|
| 偏移 | 含义 |
|
||||||
|
|---|---|
|
||||||
|
| 0x00-0x03 | `"ENet"` magic |
|
||||||
|
| 0x04-0x07 | 帧类型 u32 LE(1=发现, 2=注册/宣告, 4=数据/RDM) |
|
||||||
|
| 0x08-0x09 | 00 00(常量) |
|
||||||
|
| 0x0A-0x0B | 目标端口 u16 LE = 31772 (0x7c1c) |
|
||||||
|
| 0x0C-0x11 | 全 0 |
|
||||||
|
| 0x12-0x13 | 01 00(常量) |
|
||||||
|
| 0x14-0x15 | 帧总长 u16 LE(如 1084=0x043c, 47=0x002f, 42=0x002a) |
|
||||||
|
| 0x16-0x17 | 00 00 |
|
||||||
|
| 0x18-0x19 | 子类型 u16 LE(01=主机→Butler 数据, 02=Butler→主机应答/注册回执) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、DMX 数据帧(1084B,主机→Butler,全破解)
|
||||||
|
|
||||||
|
| 偏移 | 含义 | 备注 |
|
||||||
|
|---|---|---|
|
||||||
|
| 0x00-0x19 | 通用信封 | type=4, 子类型=01 |
|
||||||
|
| 0x1A-0x1D | 帧计数器 u32 LE | **每帧 +1,任意起始值**(1031 帧实证);与 r1/r2 无关 |
|
||||||
|
| 0x1E-0x1F | 00 00 | 常量 |
|
||||||
|
| 0x20-0x21 | 01 01 | 端口使能 mask(port1+port2) |
|
||||||
|
| 0x22-0x25 | r1 u32 LE | 随机数,port1 加密种子 |
|
||||||
|
| 0x26-0x29 | r2 u32 LE | 随机数,port2 加密种子 |
|
||||||
|
| 0x2A-0x2D | 00 00 00 00 | 常量 |
|
||||||
|
| 0x2E-0x2F | 01 00 | 常量(实测 02 也被接受 → Butler 不校验) |
|
||||||
|
| 0x30-0x31 | 00 00 | 常量 |
|
||||||
|
| 0x32-0x33 | 01 00 | 常量 |
|
||||||
|
| 0x34-0x35 | 00 02 | port1 数据长度 = 512(u16 LE) |
|
||||||
|
| 0x36-0x37 | 00 02 | port2 数据长度 = 512(u16 LE) |
|
||||||
|
| 0x38-0x3B | 01 00 00 00 | 常量 |
|
||||||
|
| 0x3C-0x23B | port1 密文 512B | `agios_dmx_encrypt(p1, r1, B12)` |
|
||||||
|
| 0x23C-0x43B | port2 密文 512B | `agios_dmx_encrypt(p2, r2, B12)` |
|
||||||
|
|
||||||
|
**加密**:`rot=(B12^r)&511; out[i]=src[(i+rot)%512]^cnt[i]`(zPprv0TXRRW 序列)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、RDM 请求帧(1084B,主机→Butler,全破解)
|
||||||
|
|
||||||
|
| 偏移 | 含义 | 备注 |
|
||||||
|
|---|---|---|
|
||||||
|
| 0x00-0x19 | 通用信封 | type=4, 子类型=01 |
|
||||||
|
| 0x1A-0x1F | **全 0** | RDM 帧计数器恒 0(424 帧实证) |
|
||||||
|
| 0x20 | **02** | RDM 请求标志(DMX 帧为 01) |
|
||||||
|
| 0x21 | 00 | 常量 |
|
||||||
|
| 0x22-0x23 | qseq u16 LE | 查询序号,逐请求 +1 |
|
||||||
|
| 0x24-0x29 | 全 0 | DMX 帧的 r1/r2 槽位,RDM 清零 |
|
||||||
|
| 0x2A | **CC** | 常量(隐式 RDM StartCode 的外显位置) |
|
||||||
|
| 0x2B-0x2D | 00 00 00 | 常量 |
|
||||||
|
| 0x2E-0x2F | 01 00 | 常量 |
|
||||||
|
| 0x30 | 00 / 01 | 成对发送标志:主帧=00,紧跟的副本=01 |
|
||||||
|
| 0x31-0x33 | 00 00 00 | 常量 |
|
||||||
|
| 0x34-0x37 | msgLen+1 u32 LE | 如 PDL=0 时 = 25 (0x19) |
|
||||||
|
| 0x38-0x3B | 01 00 00 00 | 常量 |
|
||||||
|
| 0x3C | 01 | RDM SubStartCode |
|
||||||
|
| 0x3D | msgLen | = 24+PDL(含隐式 0xCC 起始码,不含 checksum) |
|
||||||
|
| 0x3E-0x43 | 目标 UID | 广播=ff×6 |
|
||||||
|
| 0x44-0x49 | 源 UID | 主机=45 3a 7f ff ff ff(副本末字节 -1) |
|
||||||
|
| 0x4A | TN | 事务号 |
|
||||||
|
| 0x4B | port | 主帧=00,副本=01 |
|
||||||
|
| 0x4C | msgCount | 00 |
|
||||||
|
| 0x4D-0x4E | subDevice | 00 00 |
|
||||||
|
| 0x4F | CC | 命令类(0x10=Discovery, 0x20=GET, 0x30=SET) |
|
||||||
|
| 0x50-0x51 | PID | BE(0001=UNIQUE_BRANCH, 0002=MUTE, 0003=UN_MUTE, 0060=DEVICE_INFO, 00F0=DMX_START_ADDRESS…) |
|
||||||
|
| 0x52 | PDL | PD 长度 |
|
||||||
|
| 0x53.. | PD | 参数数据 |
|
||||||
|
| 0x53+PDL | checksum u16 BE | `0xCC + Σ(0x3C .. 0x53+PDL)` |
|
||||||
|
| checksum 之后 | 任意填充 | Butler 不读(实测垃圾/零均可) |
|
||||||
|
|
||||||
|
**成对副本规则**(SL 实测):主帧之后紧跟一副本,差异仅 0x30=01、0x4B=01、srcUID 末字节 -1,**checksum 不重算**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、RDM 应答帧(Butler→主机,已可解码)
|
||||||
|
|
||||||
|
- 信封子类型=02,0x1A-0x1D = `"Butl"` 标记。
|
||||||
|
- 0x28-0x29 = RDM 数据区长度 u16 LE;数据区 @0x2A。
|
||||||
|
- 数据区首字节 0xAA = DISC 脉冲(0xAA/0x55 位交织编码,checksum 在 17B 内);否则为标准 RDM 应答(CC@0, CC字段@0x14, PID@0x15-0x16, PDL@0x17, PD@0x18)。
|
||||||
|
- DEVICE_INFO PD 布局(19B):protocolVer(2) model(2) category(2) swVer(4) **footprint(2)@+10** personality(2)@+12 **DMX地址(2)@+14** 保留(2) **传感器数@+18**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、注册应答 47B(Butler→主机,线上 89B,全破解)
|
||||||
|
|
||||||
|
| 偏移 | 含义 |
|
||||||
|
|---|---|
|
||||||
|
| 0x00-0x19 | 通用信封:type=2, 子类型=02, len@0x14=47 (0x2f) |
|
||||||
|
| 0x1A-0x1D | 设备 IP(LE 逆序字节)= 192.168.123.1 |
|
||||||
|
| 0x1E-0x23 | 设备 MAC = 00 16 1c f1 2e 39 |
|
||||||
|
| 0x24 | 00 |
|
||||||
|
| 0x25-0x28 | 主机 IP(LE 逆序)= 192.168.123.113 |
|
||||||
|
| 0x29-0x2E | 主机 MAC = 30 0e d5 59 bb 36 |
|
||||||
|
|
||||||
|
## 六、42B 事件帧(Butler→主机,部分破解)
|
||||||
|
|
||||||
|
type=4, 子类型=02, len=42 (0x2a), `"Butl"`@0x1A:
|
||||||
|
- 0x20-0x23 = 事件码(已见 0x0318 / 0x0062),其余尾字节 01 00 00 00 00 00。
|
||||||
|
- 事件码语义未解码(不影响功能)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、AECH 认领(已破解)
|
||||||
|
|
||||||
|
`G(x) = x ^ TABLE[(x%32 + 1 + (x*13)%30) % 32]`;nonce/B12 主机自选随机;AERE 应答 @0xC0 == G(nonce)。
|
||||||
|
真机固件过滤:仅应答源 IP=192.168.123.113 的 AECH。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、历史误区更正(2026-09-11)
|
||||||
|
|
||||||
|
1. ~~"RDM 请求消息起始位置可变 (UN_MUTE@0x3A vs BRANCH@0x3C)"~~ → **错误**。424 帧实证全部 SubSC@0x3C,结构固定。
|
||||||
|
2. ~~"Butler 帧级校验会话一致性,verbatim 才能过"~~ → **错误**。纯自构造帧全通。之前失败根因:
|
||||||
|
- `sl_get_exact.hex` 是 DMX 数据帧被误当 RDM 模板;
|
||||||
|
- 旧 `build_rdm_frame` checksum 求和范围多算 1 字节(含模板残留的旧 checksum 槽位)。
|
||||||
|
3. ~~"0x2E 是节点序号且必须为 2"~~ → 实测 SL 原帧恒为 01,设 02 也被接受,Butler 不校验此字段。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、仍不确定(不阻塞功能)
|
||||||
|
|
||||||
|
| 项 | 状态 |
|
||||||
|
|---|---|
|
||||||
|
| 1947 端口 40B 许可令牌 | 暖会话下**非必需**(当前 show 不发也工作);Butler 冷启动/重启后是否必需未验证 |
|
||||||
|
| EUCE 6s 广播 | 暖会话下**非必需**(同上) |
|
||||||
|
| bind52 / ident102 | 暖会话下**非必需**(最小认领 announce144→AECH 即可) |
|
||||||
|
| 42B 事件码 0x0318/0x0062 语义 | 未解码 |
|
||||||
|
| NFM680S 通道数漂移(曾见 2ch/1ch) | 近期稳定 1ch,疑早期解析误判;未复现深究 |
|
||||||
|
| announce 时间戳/会话 ID 是否参与校验 | 按实测填充新鲜值即可;严格性未消融 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、纯自构造实现
|
||||||
|
|
||||||
|
`src/hostdemo.rs`:
|
||||||
|
- `build_dmx_frame()` — 零模板 DMX 帧
|
||||||
|
- `build_rdm_frame2()` — 零模板 RDM 请求帧
|
||||||
|
- `rdm_pair_copy()` — 成对副本
|
||||||
|
|
||||||
|
`HOSTDEMO_MODE=show` 已切换到纯自构造,**运行不再依赖 replay 文件或任何抓包模板**(除 announce144/claim 等静态注册帧,这些也可后续自构造化)。
|
||||||
215
src/analyze.rs
Normal file
215
src/analyze.rs
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
// analyze.rs — 离线抓包/模板分析工具
|
||||||
|
// 子命令:
|
||||||
|
// analyze dump <hexfile> <start> <end> 按偏移 dump 十六进制
|
||||||
|
// analyze diff <file1> <file2> 逐字节对比两个 hex 文件
|
||||||
|
// analyze hdr replay DMX/RDM 帧头字段方差分析
|
||||||
|
// analyze rdm replay 中 RDM 请求帧结构分析 (变长前置区)
|
||||||
|
// analyze ctr DMX 帧计数器序列分析
|
||||||
|
|
||||||
|
pub fn load_hex(path: &str) -> Vec<u8> {
|
||||||
|
let s = std::fs::read_to_string(path).unwrap_or_else(|_| panic!("无法读取 {}", path));
|
||||||
|
let s: String = s.chars().filter(|c| c.is_ascii_hexdigit()).collect();
|
||||||
|
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_replay(path: &str) -> Vec<Vec<u8>> {
|
||||||
|
let data = std::fs::read_to_string(path).unwrap_or_else(|_| panic!("无法读取 {}", path));
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for line in data.lines() {
|
||||||
|
let p: Vec<&str> = line.split('|').collect();
|
||||||
|
if p.len() < 4 { continue; }
|
||||||
|
let hex = p[3].trim();
|
||||||
|
if hex.len() % 2 != 0 { continue; }
|
||||||
|
let mut b = Vec::with_capacity(hex.len() / 2);
|
||||||
|
let mut ok = true;
|
||||||
|
for i in (0..hex.len()).step_by(2) {
|
||||||
|
match u8::from_str_radix(&hex[i..i + 2], 16) {
|
||||||
|
Ok(v) => b.push(v),
|
||||||
|
Err(_) => { ok = false; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ok { out.push(b); }
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hx(b: &[u8]) -> String {
|
||||||
|
b.iter().map(|x| format!("{:02x}", x)).collect::<Vec<_>>().join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_replay() -> String {
|
||||||
|
std::env::var("REPLAY_FILE")
|
||||||
|
.unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dump(args: &[String]) {
|
||||||
|
let b = load_hex(&args[0]);
|
||||||
|
let start: usize = args[1].trim_start_matches("0x").parse().unwrap_or_else(|_| usize::from_str_radix(args[1].trim_start_matches("0x"), 16).unwrap());
|
||||||
|
let end: usize = args.get(2).map(|s| s.trim_start_matches("0x").parse().unwrap_or_else(|_| usize::from_str_radix(s.trim_start_matches("0x"), 16).unwrap())).unwrap_or(b.len());
|
||||||
|
println!("{} len={} bytes", args[0], b.len());
|
||||||
|
let mut i = start;
|
||||||
|
while i < end.min(b.len()) {
|
||||||
|
let e = (i + 16).min(end).min(b.len());
|
||||||
|
println!(" 0x{:03x}: {}", i, hx(&b[i..e]));
|
||||||
|
i = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(args: &[String]) {
|
||||||
|
let a = load_hex(&args[0]);
|
||||||
|
let b = load_hex(&args[1]);
|
||||||
|
println!("A: {} ({} bytes)", args[0], a.len());
|
||||||
|
println!("B: {} ({} bytes)", args[1], b.len());
|
||||||
|
let n = a.len().min(b.len());
|
||||||
|
let diffs: Vec<usize> = (0..n).filter(|&i| a[i] != b[i]).collect();
|
||||||
|
println!("共同前缀 {} bytes 中差异 {} 处:", n, diffs.len());
|
||||||
|
for i in &diffs {
|
||||||
|
println!(" 0x{:03x}: A={:02x} B={:02x}", i, a[*i], b[*i]);
|
||||||
|
}
|
||||||
|
if a.len() != b.len() { println!("长度不同: A={} B={}", a.len(), b.len()); }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hdr() {
|
||||||
|
let frames = load_replay(&default_replay());
|
||||||
|
println!("replay 帧总数: {}", frames.len());
|
||||||
|
// 分类
|
||||||
|
let dmx: Vec<&Vec<u8>> = frames.iter()
|
||||||
|
.filter(|f| f.len() == 1084 && f[0x18] == 1 && f[0x20] == 1 && f[0x21] == 1)
|
||||||
|
.collect();
|
||||||
|
let rdm: Vec<&Vec<u8>> = frames.iter()
|
||||||
|
.filter(|f| f.len() == 1084 && f[0x18] == 1 && f[0x20] == 2)
|
||||||
|
.collect();
|
||||||
|
println!("DMX 数据帧: {}, RDM 请求帧(0x3C==01): {}", dmx.len(), rdm.len());
|
||||||
|
|
||||||
|
if let Some(f0) = dmx.first() {
|
||||||
|
println!("\n=== DMX 帧头 (帧#1) ===");
|
||||||
|
for r in (0..0x3C).step_by(16) {
|
||||||
|
println!(" 0x{:03x}: {}", r, hx(&f0[r..(r + 16).min(f0.len())]));
|
||||||
|
}
|
||||||
|
// 每偏移方差
|
||||||
|
println!("\n=== DMX 帧头逐偏移方差 (0x00-0x3B) ===");
|
||||||
|
for i in 0..0x3C {
|
||||||
|
let mut vals: Vec<u8> = dmx.iter().map(|f| f[i]).collect();
|
||||||
|
vals.sort();
|
||||||
|
vals.dedup();
|
||||||
|
if vals.len() == 1 {
|
||||||
|
println!(" 0x{:03x}: 常量 {:02x}", i, vals[0]);
|
||||||
|
} else if vals.len() <= 4 {
|
||||||
|
println!(" 0x{:03x}: {}种 {:?}", i, vals.len(), vals);
|
||||||
|
} else {
|
||||||
|
let min = vals.first().unwrap();
|
||||||
|
let max = vals.last().unwrap();
|
||||||
|
println!(" 0x{:03x}: 变量 {}种 [{:02x}..{:02x}]", i, vals.len(), min, max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(f0) = rdm.first() {
|
||||||
|
println!("\n=== RDM 请求帧头+消息区 (帧#1) ===");
|
||||||
|
for r in (0..0x70).step_by(16) {
|
||||||
|
println!(" 0x{:03x}: {}", r, hx(&f0[r..(r + 16).min(f0.len())]));
|
||||||
|
}
|
||||||
|
println!("\n=== RDM 帧头逐偏移方差 (0x00-0x3B) ===");
|
||||||
|
for i in 0..0x3C {
|
||||||
|
let mut vals: Vec<u8> = rdm.iter().map(|f| f[i]).collect();
|
||||||
|
vals.sort();
|
||||||
|
vals.dedup();
|
||||||
|
if vals.len() == 1 {
|
||||||
|
println!(" 0x{:03x}: 常量 {:02x}", i, vals[0]);
|
||||||
|
} else if vals.len() <= 4 {
|
||||||
|
println!(" 0x{:03x}: {}种 {:?}", i, vals.len(), vals);
|
||||||
|
} else {
|
||||||
|
let min = vals.first().unwrap();
|
||||||
|
let max = vals.last().unwrap();
|
||||||
|
println!(" 0x{:03x}: 变量 {}种 [{:02x}..{:02x}]", i, vals.len(), min, max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rdm() {
|
||||||
|
let frames = load_replay(&default_replay());
|
||||||
|
let rdm: Vec<&Vec<u8>> = frames.iter()
|
||||||
|
.filter(|f| f.len() == 1084 && f[0x18] == 1 && f[0x20] == 2)
|
||||||
|
.collect();
|
||||||
|
println!("RDM 请求帧(0x20==02): {}", rdm.len());
|
||||||
|
// 按 PID 分组 (假设 RDM @0x3C: CC@0x4F, PID@0x50-51)
|
||||||
|
let mut by_pid: std::collections::HashMap<u16, Vec<&Vec<u8>>> = std::collections::HashMap::new();
|
||||||
|
for f in &rdm {
|
||||||
|
let pid = ((f[0x50] as u16) << 8) | f[0x51] as u16;
|
||||||
|
by_pid.entry(pid).or_default().push(f);
|
||||||
|
}
|
||||||
|
let mut pids: Vec<u16> = by_pid.keys().cloned().collect();
|
||||||
|
pids.sort();
|
||||||
|
for pid in &pids {
|
||||||
|
let g = &by_pid[pid];
|
||||||
|
println!("\n=== PID=0x{:04x} ({} 帧) ===", pid, g.len());
|
||||||
|
for (k, f) in g.iter().take(2).enumerate() {
|
||||||
|
println!(" 样本{}:", k + 1);
|
||||||
|
for r in (0x2A..0x70).step_by(16) {
|
||||||
|
println!(" 0x{:03x}: {}", r, hx(&f[r..(r + 16).min(f.len())]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 消息长度字段候选: 0x3D (msgLen), 0x34-0x38
|
||||||
|
let ml: Vec<u8> = g.iter().map(|f| f[0x3D]).collect();
|
||||||
|
let mut u: Vec<u8> = ml.clone();
|
||||||
|
u.sort(); u.dedup();
|
||||||
|
println!(" 0x3D(msgLen)取值: {:?}", u);
|
||||||
|
}
|
||||||
|
// 检查是否存在 RDM 起始不在 0x3C 的帧 (其它 0x20 取值)
|
||||||
|
let mut by_20: std::collections::HashMap<u8, usize> = std::collections::HashMap::new();
|
||||||
|
for f in &frames {
|
||||||
|
if f.len() == 1084 && f[0x18] == 1 {
|
||||||
|
*by_20.entry(f[0x20]).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("\n0x20 取值分布: {:?}", by_20);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ctr() {
|
||||||
|
let frames = load_replay(&default_replay());
|
||||||
|
let dmx: Vec<&Vec<u8>> = frames.iter()
|
||||||
|
.filter(|f| f.len() == 1084 && f[0x18] == 1 && f[0x20] == 1 && f[0x21] == 1)
|
||||||
|
.collect();
|
||||||
|
println!("DMX 数据帧: {}", dmx.len());
|
||||||
|
let ctrs: Vec<u32> = dmx.iter()
|
||||||
|
.map(|f| u32::from_le_bytes([f[0x1A], f[0x1B], f[0x1C], f[0x1D]]))
|
||||||
|
.collect();
|
||||||
|
println!("首个计数器: {}, 末个: {}", ctrs.first().unwrap_or(&0), ctrs.last().unwrap_or(&0));
|
||||||
|
// 增量分布
|
||||||
|
let mut deltas: std::collections::HashMap<u32, u32> = std::collections::HashMap::new();
|
||||||
|
for w in ctrs.windows(2) {
|
||||||
|
let d = w[1].wrapping_sub(w[0]);
|
||||||
|
*deltas.entry(d).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
let mut ds: Vec<(u32, u32)> = deltas.into_iter().collect();
|
||||||
|
ds.sort_by_key(|(_, n)| std::cmp::Reverse(*n));
|
||||||
|
println!("增量分布 (前10):");
|
||||||
|
for (d, n) in ds.iter().take(10) {
|
||||||
|
println!(" +{}: {} 次", d, n);
|
||||||
|
}
|
||||||
|
// RDM 帧的计数器
|
||||||
|
let rdm: Vec<&Vec<u8>> = frames.iter()
|
||||||
|
.filter(|f| f.len() == 1084 && f[0x18] == 1 && f[0x20] == 2)
|
||||||
|
.collect();
|
||||||
|
let rctrs: Vec<u32> = rdm.iter()
|
||||||
|
.map(|f| u32::from_le_bytes([f[0x1A], f[0x1B], f[0x1C], f[0x1D]]))
|
||||||
|
.collect();
|
||||||
|
println!("RDM 帧计数器范围: {:?} .. {:?} (共{}帧)",
|
||||||
|
rctrs.first(), rctrs.last(), rctrs.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(args: &[String]) {
|
||||||
|
if args.is_empty() {
|
||||||
|
println!("analyze dump <hexfile> <start> [end] | diff <a> <b> | hdr | rdm | ctr");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match args[0].as_str() {
|
||||||
|
"dump" => dump(&args[1..]),
|
||||||
|
"diff" => diff(&args[1..]),
|
||||||
|
"hdr" => hdr(),
|
||||||
|
"rdm" => rdm(),
|
||||||
|
"ctr" => ctr(),
|
||||||
|
_ => println!("未知 analyze 子命令 {}", args[0]),
|
||||||
|
}
|
||||||
|
}
|
||||||
120
src/hostdemo.rs
120
src/hostdemo.rs
|
|
@ -102,6 +102,77 @@ fn build_rdm_frame(dest: &[u8; 6], tn: u8, qseq: u16, cc: u8, pid: u16, pd: &[u8
|
||||||
f
|
f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ★从零构造 DMX 数据帧 (1084B, 全字段已破解 — analyze hdr 实证):
|
||||||
|
/// 帧头全部常量已知, 仅 0x1A 计数器(+1/帧) 与 0x22/0x26 r1/r2(加密随机) 变化
|
||||||
|
fn build_dmx_frame(p1: &[u8; 512], p2: &[u8; 512], r1: u32, r2: u32, b12: u32, ctr: u32) -> Vec<u8> {
|
||||||
|
let mut f = vec![0u8; 1084];
|
||||||
|
f[0..4].copy_from_slice(b"ENet");
|
||||||
|
f[4] = 4; // type=4
|
||||||
|
f[0x0A] = 0x1c; f[0x0B] = 0x7c; // 目标端口 31772 (LE)
|
||||||
|
f[0x12] = 1; // 常量 01 00
|
||||||
|
f[0x14] = 0x3c; f[0x15] = 0x04; // 帧长 1084 (u16 LE)
|
||||||
|
f[0x18] = 1; // 常量 01 00
|
||||||
|
f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes()); // 帧计数器 (+1/帧, 任意起始)
|
||||||
|
f[0x20] = 1; f[0x21] = 1; // 端口使能 mask: port1+port2
|
||||||
|
f[0x22..0x26].copy_from_slice(&r1.to_le_bytes());
|
||||||
|
f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes());
|
||||||
|
f[0x2E] = 1; // 常量 (实测 01; 02 也能被接受)
|
||||||
|
f[0x32] = 1; // 常量
|
||||||
|
f[0x35] = 2; f[0x37] = 2; // port1/port2 数据长度 = 512 (u16 LE ×2)
|
||||||
|
f[0x38] = 1; // 常量 01 00 00 00
|
||||||
|
f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(p1, r1, b12));
|
||||||
|
f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(p2, r2, b12));
|
||||||
|
f
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ★从零构造 RDM 请求帧 (1084B, 全字段已破解 — analyze rdm 实证):
|
||||||
|
/// 计数器恒 0; 0x20=02 为 RDM 请求标志; 0x2A=0xCC 常量; checksum=0xCC+Σ(0x3C..0x53+PDL) @0x53+PDL
|
||||||
|
/// msgLen=24+PDL (含隐式 0xCC 起始码); checksum 后任意填充 (Butler 不读)
|
||||||
|
fn build_rdm_frame2(dest: &[u8; 6], tn: u8, qseq: u16, cc: u8, pid: u16, pd: &[u8]) -> Vec<u8> {
|
||||||
|
let msg_len = 24 + pd.len();
|
||||||
|
let mut f = vec![0u8; 1084];
|
||||||
|
f[0..4].copy_from_slice(b"ENet");
|
||||||
|
f[4] = 4;
|
||||||
|
f[0x0A] = 0x1c; f[0x0B] = 0x7c;
|
||||||
|
f[0x12] = 1;
|
||||||
|
f[0x14] = 0x3c; f[0x15] = 0x04;
|
||||||
|
f[0x18] = 1;
|
||||||
|
// 0x1A-0x1F: 计数器恒 0
|
||||||
|
f[0x20] = 2; // RDM 请求标志
|
||||||
|
f[0x22..0x24].copy_from_slice(&qseq.to_le_bytes());
|
||||||
|
f[0x2A] = 0xCC; // 常量
|
||||||
|
f[0x2E] = 1;
|
||||||
|
// 0x30: 00 主帧 (成对副本=01)
|
||||||
|
f[0x34..0x38].copy_from_slice(&((msg_len + 1) as u32).to_le_bytes());
|
||||||
|
f[0x38] = 1;
|
||||||
|
// RDM 消息 @0x3C: SubSC/msgLen/dest/src/TN/port/msgCount/subDev/CC/PID/PDL/PD
|
||||||
|
f[0x3C] = 1;
|
||||||
|
f[0x3D] = msg_len as u8;
|
||||||
|
f[0x3E..0x44].copy_from_slice(dest);
|
||||||
|
f[0x44..0x4A].copy_from_slice(&HOST_RDM_UID);
|
||||||
|
f[0x4A] = tn;
|
||||||
|
f[0x4F] = cc;
|
||||||
|
f[0x50] = (pid >> 8) as u8;
|
||||||
|
f[0x51] = (pid & 0xFF) as u8;
|
||||||
|
f[0x52] = pd.len() as u8;
|
||||||
|
f[0x53..0x53 + pd.len()].copy_from_slice(pd);
|
||||||
|
let mut sum: u16 = 0xCC;
|
||||||
|
for i in 0x3C..0x53 + pd.len() { sum = sum.wrapping_add(f[i] as u16); }
|
||||||
|
let cs = 0x53 + pd.len();
|
||||||
|
f[cs] = (sum >> 8) as u8;
|
||||||
|
f[cs + 1] = (sum & 0xFF) as u8;
|
||||||
|
f
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RDM 成对副本 (SL 实测: flag@0x30=01, port@0x4B=01, srcUID 末字节-1, checksum 不重算)
|
||||||
|
fn rdm_pair_copy(f: &[u8]) -> Vec<u8> {
|
||||||
|
let mut f2 = f.to_vec();
|
||||||
|
f2[0x30] = 0x01;
|
||||||
|
f2[0x4B] = 0x01;
|
||||||
|
f2[0x49] = f2[0x49].wrapping_sub(1);
|
||||||
|
f2
|
||||||
|
}
|
||||||
|
|
||||||
/// DISC 脉冲解码 (§5.4 逆运算): data = RDM 数据区 (0x2A 起), 返回 Some(UID) 或 None(冲突/非法)
|
/// DISC 脉冲解码 (§5.4 逆运算): data = RDM 数据区 (0x2A 起), 返回 Some(UID) 或 None(冲突/非法)
|
||||||
fn decode_pulse(d: &[u8]) -> Option<[u8; 6]> {
|
fn decode_pulse(d: &[u8]) -> Option<[u8; 6]> {
|
||||||
if d.len() < 17 || d[0] != 0xAA { return None; }
|
if d.len() < 17 || d[0] != 0xAA { return None; }
|
||||||
|
|
@ -375,11 +446,13 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
|
||||||
if mode == "hybrid" || mode == "show" {
|
if mode == "hybrid" || mode == "show" {
|
||||||
let is_show = mode == "show";
|
let is_show = mode == "show";
|
||||||
let label = if is_show { "SHOW" } else { "H" };
|
let label = if is_show { "SHOW" } else { "H" };
|
||||||
|
// show: 全字段已破解, 零模板纯自构造; hybrid: 仍用 SL 实帧骨架做对照
|
||||||
|
let mut dmxf: Vec<Vec<u8>> = Vec::new();
|
||||||
|
let mut base_rdm: Option<Vec<u8>> = None;
|
||||||
|
if !is_show {
|
||||||
let path = std::env::var("REPLAY_FILE")
|
let path = std::env::var("REPLAY_FILE")
|
||||||
.unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into());
|
.unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into());
|
||||||
let data = std::fs::read_to_string(&path).expect("replay file");
|
let data = std::fs::read_to_string(&path).expect("replay file");
|
||||||
let mut dmxf: Vec<Vec<u8>> = Vec::new(); // verbatim DMX 帧
|
|
||||||
let mut base_rdm: Option<Vec<u8>> = None;
|
|
||||||
for line in data.lines() {
|
for line in data.lines() {
|
||||||
let p: Vec<&str> = line.split('|').collect();
|
let p: Vec<&str> = line.split('|').collect();
|
||||||
if p.len() < 4 { continue; }
|
if p.len() < 4 { continue; }
|
||||||
|
|
@ -397,6 +470,7 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
|
||||||
base_rdm = Some(fb);
|
base_rdm = Some(fb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 认领 (announce144 → AECH)
|
// 认领 (announce144 → AECH)
|
||||||
let o2: Vec<u8> = bind_ip.split('.').map(|x| x.parse().unwrap()).collect();
|
let o2: Vec<u8> = bind_ip.split('.').map(|x| x.parse().unwrap()).collect();
|
||||||
sock.send_to(&load_template("announce144"), &dst).unwrap();
|
sock.send_to(&load_template("announce144"), &dst).unwrap();
|
||||||
|
|
@ -409,10 +483,9 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
|
||||||
ch[0x8C..0x90].copy_from_slice(&bc.to_le_bytes());
|
ch[0x8C..0x90].copy_from_slice(&bc.to_le_bytes());
|
||||||
sock.send_to(&ch, &dst).unwrap();
|
sock.send_to(&ch, &dst).unwrap();
|
||||||
std::thread::sleep(Duration::from_millis(400));
|
std::thread::sleep(Duration::from_millis(400));
|
||||||
println!("[{}] 认领完成 B12=0x{:08x}; {} DMX 帧骨架 + 我们的 RDM", label, bc,
|
println!("[{}] 认领完成 B12=0x{:08x}; {}", label, bc,
|
||||||
if is_show { "地址1-32 炫彩呼吸" } else { "verbatim" });
|
if is_show { "地址1-32 炫彩呼吸 (零模板纯自构造 DMX+RDM)" } else { "verbatim DMX + 模板 RDM" });
|
||||||
let rdm_t = base_rdm.expect("no RDM template");
|
let rdm_t: Option<Vec<u8>> = base_rdm;
|
||||||
let dmx_a = load_template("dmx_a"); // show 模式 DMX 用此模板(已验证 HMKJ 能解析)
|
|
||||||
let known: [[u8; 6]; 3] = [
|
let known: [[u8; 6]; 3] = [
|
||||||
[0x01, 0x07, 0x34, 0x67, 0x01, 0x25],
|
[0x01, 0x07, 0x34, 0x67, 0x01, 0x25],
|
||||||
[0x01, 0x07, 0x34, 0x88, 0x09, 0x00],
|
[0x01, 0x07, 0x34, 0x88, 0x09, 0x00],
|
||||||
|
|
@ -420,6 +493,7 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
|
||||||
];
|
];
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let mut di = 0usize;
|
let mut di = 0usize;
|
||||||
|
let mut qseq: u16 = 0;
|
||||||
let mut last_rdm = Instant::now() - Duration::from_secs(10);
|
let mut last_rdm = Instant::now() - Duration::from_secs(10);
|
||||||
let mut step = 0usize;
|
let mut step = 0usize;
|
||||||
let mut addr_list: Vec<u16> = Vec::new();
|
let mut addr_list: Vec<u16> = Vec::new();
|
||||||
|
|
@ -457,25 +531,24 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
|
||||||
}
|
}
|
||||||
let rr1 = rng.next();
|
let rr1 = rng.next();
|
||||||
let rr2 = rng.next();
|
let rr2 = rng.next();
|
||||||
let mut f = if is_show {
|
let f = if is_show {
|
||||||
let mut fa = dmx_a.clone();
|
// ★纯自构造 DMX 帧 (零模板)
|
||||||
fa[0x2E] = 2; fa[0x2F] = 0; // 节点序号 (真机 .1 = 2)
|
build_dmx_frame(&p1, &[0u8; 512], rr1, rr2, bc, di as u32)
|
||||||
fa
|
|
||||||
} else {
|
} else {
|
||||||
dmxf[di % dmxf.len()].clone()
|
let mut fx = dmxf[di % dmxf.len()].clone();
|
||||||
|
fx[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes());
|
||||||
|
fx[0x22..0x26].copy_from_slice(&rr1.to_le_bytes());
|
||||||
|
fx[0x26..0x2A].copy_from_slice(&rr2.to_le_bytes());
|
||||||
|
fx[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, rr1, bc));
|
||||||
|
fx[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&[0u8; 512], rr2, bc));
|
||||||
|
fx
|
||||||
};
|
};
|
||||||
f[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes());
|
|
||||||
f[0x22..0x26].copy_from_slice(&rr1.to_le_bytes());
|
|
||||||
f[0x26..0x2A].copy_from_slice(&rr2.to_le_bytes());
|
|
||||||
f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, rr1, bc));
|
|
||||||
f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&[0u8; 512], rr2, bc));
|
|
||||||
di += 1;
|
di += 1;
|
||||||
let _ = sock.send_to(&f, &dst);
|
let _ = sock.send_to(&f, &dst);
|
||||||
if di % 30 == 0 { let _ = sock.send_to(&load_template("heartbeat"), &dst); }
|
if di % 30 == 0 { let _ = sock.send_to(&load_template("heartbeat"), &dst); }
|
||||||
// RDM 任务
|
// RDM 任务
|
||||||
if t > 2.0 && last_rdm.elapsed() >= Duration::from_millis(700) {
|
if t > 2.0 && last_rdm.elapsed() >= Duration::from_millis(700) {
|
||||||
last_rdm = Instant::now();
|
last_rdm = Instant::now();
|
||||||
let mut f = rdm_t.clone();
|
|
||||||
let mut pd: Vec<u8> = Vec::new();
|
let mut pd: Vec<u8> = Vec::new();
|
||||||
let mut pid: u16 = 0x0001;
|
let mut pid: u16 = 0x0001;
|
||||||
let mut dest = [0xffu8; 6];
|
let mut dest = [0xffu8; 6];
|
||||||
|
|
@ -492,11 +565,20 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
|
||||||
else { step = 3; }
|
else { step = 3; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let tn = (step * 7 + 1) as u8;
|
||||||
|
let (f, f2) = if is_show {
|
||||||
|
// ★纯自构造 RDM 帧 (零模板) + 成对副本
|
||||||
|
qseq = qseq.wrapping_add(1);
|
||||||
|
let f = build_rdm_frame2(&dest, tn, qseq, cc, pid, &pd);
|
||||||
|
let f2 = rdm_pair_copy(&f);
|
||||||
|
(f, f2)
|
||||||
|
} else {
|
||||||
|
let mut f = rdm_t.clone().unwrap();
|
||||||
let msg_len = 24 + pd.len();
|
let msg_len = 24 + pd.len();
|
||||||
f[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes());
|
f[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes());
|
||||||
f[0x3D] = msg_len as u8;
|
f[0x3D] = msg_len as u8;
|
||||||
f[0x3E..0x44].copy_from_slice(&dest);
|
f[0x3E..0x44].copy_from_slice(&dest);
|
||||||
f[0x4A] = (step * 7 + 1) as u8;
|
f[0x4A] = tn;
|
||||||
f[0x4F] = cc;
|
f[0x4F] = cc;
|
||||||
f[0x50] = (pid >> 8) as u8;
|
f[0x50] = (pid >> 8) as u8;
|
||||||
f[0x51] = (pid & 0xFF) as u8;
|
f[0x51] = (pid & 0xFF) as u8;
|
||||||
|
|
@ -510,6 +592,8 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
|
||||||
let mut f2 = f.clone();
|
let mut f2 = f.clone();
|
||||||
f2[0x30] = 0x01;
|
f2[0x30] = 0x01;
|
||||||
f2[0x49] = f2[0x49].wrapping_sub(1);
|
f2[0x49] = f2[0x49].wrapping_sub(1);
|
||||||
|
(f, f2)
|
||||||
|
};
|
||||||
let _ = sock.send_to(&f, &dst);
|
let _ = sock.send_to(&f, &dst);
|
||||||
let _ = sock.send_to(&f2, &dst);
|
let _ = sock.send_to(&f2, &dst);
|
||||||
println!("[{}] RDM step={} pid={:04x}", label, step, pid);
|
println!("[{}] RDM step={} pid={:04x}", label, step, pid);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ mod tests;
|
||||||
mod dmxhash;
|
mod dmxhash;
|
||||||
mod aech;
|
mod aech;
|
||||||
mod hostdemo;
|
mod hostdemo;
|
||||||
|
mod analyze;
|
||||||
|
|
||||||
use tests::Item;
|
use tests::Item;
|
||||||
|
|
||||||
|
|
@ -168,6 +169,12 @@ fn main() {
|
||||||
hostdemo::run(&ip, "192.168.123.113", secs);
|
hostdemo::run(&ip, "192.168.123.113", secs);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
"analyze" => {
|
||||||
|
// analyze dump|diff|hdr|rdm|ctr — 离线抓包/模板分析
|
||||||
|
let args: Vec<String> = std::env::args().skip(2).collect();
|
||||||
|
analyze::run(&args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
println!("用法: enet-validator <discover|announce|heartbeat|euce|session [秒]|fallback|claim|rdm [秒]|all>");
|
println!("用法: enet-validator <discover|announce|heartbeat|euce|session [秒]|fallback|claim|rdm [秒]|all>");
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user