feat: udprelay subcommand + HOSTDEMO_LISTEN env for cross-subnet host verification

- udprelay [listen] [butler_ip] [src_ip]: generic UDP/31772 relay on Windows,
  forwards client frames to Butler with spoofed source IP (default .113,
  bypasses AECH firmware filter), relays Butler replies/broadcasts back
- hostdemo: HOSTDEMO_LISTEN separates actual socket bind from advertised IP
  (Ubuntu binds 0.0.0.0:31772, frames still claim 192.168.123.113)
This commit is contained in:
OpenCode 2026-09-11 18:37:03 +08:00
parent 4d01cbf6ea
commit e03d130f0f
2 changed files with 53 additions and 2 deletions

View File

@ -207,12 +207,15 @@ fn rdm_region(b: &[u8]) -> Option<(bool, &[u8])> {
pub fn run(target: &str, bind_ip: &str, secs: u64) { pub fn run(target: &str, bind_ip: &str, secs: u64) {
let dst = format!("{}:31772", target); let dst = format!("{}:31772", target);
let sock = open_reuse(&format!("{}:31772", bind_ip)); // 源 123.113:31772 (真机过滤+主机惯例) // HOSTDEMO_LISTEN: 实际 socket 绑定地址 (默认=bind_ip:31772)。
// 跨网段经 udprelay 中继时, Ubuntu 侧须绑本地地址 (如 0.0.0.0:31772), 帧内宣告 IP 仍为 bind_ip (123.113)
let listen = std::env::var("HOSTDEMO_LISTEN").unwrap_or_else(|_| format!("{}:31772", bind_ip));
let sock = open_reuse(&listen); // 源 123.113:31772 (真机过滤+主机惯例)
sock.set_read_timeout(Some(Duration::from_millis(5))).unwrap(); sock.set_read_timeout(Some(Duration::from_millis(5))).unwrap();
let mut rng = Rng::new(); let mut rng = Rng::new();
let mut buf = [0u8; 4096]; let mut buf = [0u8; 4096];
println!("=== 主机侧等效示例: 目标 {} (源 {}) ===", dst, bind_ip); println!("=== 主机侧等效示例: 目标 {} (源 {}, 监听 {}) ===", dst, bind_ip, listen);
// ---- 1) 发现 ---- // ---- 1) 发现 ----
let disc = load_template("discovery_req"); let disc = load_template("discovery_req");
@ -1886,3 +1889,43 @@ fn session_loop(sock: &UdpSocket, dst: &str, target: &str, rng: &mut Rng, b12: u
} }
println!("[S] 会话环结束: DMX {} 帧, 发现灯具 {} 台, 控制地址 {:?}", sent, fixtures.len(), addr_list); println!("[S] 会话环结束: DMX {} 帧, 发现灯具 {} 台, 控制地址 {:?}", sent, fixtures.len(), addr_list);
} }
/// ★UDP 31772 通用中继 (udprelay 子命令): 跨网段主机验证用
/// 客户端(任意网段) ↔ sockA(listen_addr) | sockB(src_ip:0) ↔ Butler
/// 发往 Butler 的帧源 IP = src_ip (123.113, 绕过 AECH 固件源过滤);
/// Butler 应答(单播到 sockB) 转发回最后一个客户端; Butler 广播(到 sockA) 也转发。
pub fn udp_relay(listen_addr: &str, butler_ip: &str, src_ip: &str) {
let a = open_reuse(listen_addr);
let b = open_reuse(&format!("{}:0", src_ip));
a.set_read_timeout(Some(Duration::from_millis(2))).unwrap();
b.set_read_timeout(Some(Duration::from_millis(2))).unwrap();
let bdst = format!("{}:31772", butler_ip);
let mut client: Option<std::net::SocketAddr> = None;
let mut ba = [0u8; 4096];
let mut bb = [0u8; 4096];
let (mut n_fwd, mut n_rev) = (0u64, 0u64);
println!("[RELAY] {} <-> Butler {} (源伪装 {}), Ctrl+C 停止", listen_addr, bdst, src_ip);
loop {
if let Ok((n, from)) = a.recv_from(&mut ba) {
if from.ip().to_string() == butler_ip {
// Butler 广播/事件 → 客户端
if let Some(c) = client { let _ = a.send_to(&ba[..n], c); n_rev += 1; }
} else {
client = Some(from);
let _ = b.send_to(&ba[..n], &bdst);
n_fwd += 1;
if n_fwd == 1 { println!("[RELAY] 客户端 = {}", from); }
}
}
if let Ok((n, _from)) = b.recv_from(&mut bb) {
if let Some(c) = client {
let _ = a.send_to(&bb[..n], c);
n_rev += 1;
}
}
if (n_fwd + n_rev) % 2000 == 1 && n_fwd > 1 {
println!("[RELAY] 已转发 客户端→Butler {} 帧, Butler→客户端 {} 帧", n_fwd, n_rev);
}
}
}

View File

@ -83,6 +83,14 @@ fn main() {
} }
return; return;
} }
"udprelay" => {
// udprelay [listen_addr] [butler_ip] [src_ip] — UDP/31772 通用中继 (跨网段主机验证)
let listen = std::env::args().nth(2).unwrap_or_else(|| "0.0.0.0:31772".into());
let butler = std::env::args().nth(3).unwrap_or_else(|| "192.168.123.1".into());
let src = std::env::args().nth(4).unwrap_or_else(|| "192.168.123.113".into());
hostdemo::udp_relay(&listen, &butler, &src);
return;
}
"all" => { "all" => {
results.push(tests::t1_discover()); results.push(tests::t1_discover());
results.push(tests::tx_observe("T2", "announce", false)); results.push(tests::tx_observe("T2", "announce", false));