Skip to content

Commit a182014

Browse files
committed
feat: 添加全国对战 UDP→TCP 中继
通过 IAT Hook WSASendTo/WSARecvFrom,将游戏对战 UDP 包转换为 TCP 帧经 reflector 中继。 支持 Holdpunch/Reflector/Tunnel/Music 四种帧类型,帧格式 type(1B)+length(2B LE)+payload。
1 parent 59e5b57 commit a182014

4 files changed

Lines changed: 367 additions & 0 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ toml = "0.8"
1414
once_cell = "1"
1515
windows-sys = { version = "0.52", features = [
1616
"Win32_Foundation",
17+
"Win32_Networking_WinSock",
1718
"Win32_Devices_Communication",
1819
"Win32_Security",
1920
"Win32_Storage_FileSystem",

src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ fast_restart = true
106106
## 网络请求日志(诊断用:输出游戏的 WinHTTP 请求到日志)
107107
#[NetLog]
108108
109+
## 全国对战(将对战 UDP 转为 TCP 经服务端中继,reflector 地址由服务端下发)
110+
#[NationalMatch]
111+
109112
## 自动游玩
110113
#[Autoplay]
111114
#hotkey = "Home"

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ mod gfx;
1616
mod io4;
1717
mod iohook;
1818
mod led;
19+
mod national_match;
1920
mod patch_engine;
2021
mod patches;
2122
mod platform;
@@ -139,6 +140,7 @@ pub extern "C" fn chumod_init(info: *const ChuModInfo, api: *const ChuModAPI) ->
139140
}
140141

141142
patches::apply_all(api, &config);
143+
national_match::init(api, &config);
142144
autoplay::init_all(api, &config);
143145
ux::init_all(api, &config);
144146
d3d9::init_all(api, &config);

src/national_match/mod.rs

Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
use std::ffi::c_void;
2+
use std::net::TcpStream;
3+
use std::sync::Mutex;
4+
5+
use once_cell::sync::OnceCell;
6+
use windows_sys::Win32::Networking::WinSock::{SOCKADDR, SOCKADDR_IN, WSABUF};
7+
8+
use crate::config::Config;
9+
use crate::util::api::Api;
10+
use crate::util::iat_hook::hook_iat;
11+
12+
const SECTION: &str = "NationalMatch";
13+
const WS2_32: &str = "ws2_32.dll";
14+
15+
const TYPE_HOLDPUNCH: u8 = 0;
16+
const TYPE_REFLECTOR: u8 = 1;
17+
const TYPE_TUNNEL: u8 = 2;
18+
const TYPE_MUSIC: u8 = 4;
19+
20+
const ISFINISH_FALSE: &[u8] = b"{\"isFinish\":false}\0";
21+
22+
type WsaSendToFn = unsafe extern "system" fn(
23+
usize,
24+
*const WSABUF,
25+
u32,
26+
*mut u32,
27+
u32,
28+
*const SOCKADDR,
29+
i32,
30+
*mut c_void,
31+
*mut c_void,
32+
) -> i32;
33+
34+
type WsaRecvFromFn = unsafe extern "system" fn(
35+
usize,
36+
*const WSABUF,
37+
u32,
38+
*mut u32,
39+
*mut u32,
40+
*mut SOCKADDR,
41+
*mut i32,
42+
*mut c_void,
43+
*mut c_void,
44+
) -> i32;
45+
46+
struct State {
47+
reflector: Mutex<Option<TcpStream>>,
48+
reflector_addr: Mutex<Option<[u8; 4]>>,
49+
reflector_port: Mutex<u16>,
50+
music_sent: Mutex<bool>,
51+
}
52+
53+
static STATE: OnceCell<State> = OnceCell::new();
54+
static ORIG_SENDTO: OnceCell<WsaSendToFn> = OnceCell::new();
55+
static ORIG_RECVFROM: OnceCell<WsaRecvFromFn> = OnceCell::new();
56+
57+
pub fn init(api: &Api, config: &Config) {
58+
if !config.is_enabled(SECTION) {
59+
return;
60+
}
61+
62+
let _ = STATE.set(State {
63+
reflector: Mutex::new(None),
64+
reflector_addr: Mutex::new(None),
65+
reflector_port: Mutex::new(0),
66+
music_sent: Mutex::new(false),
67+
});
68+
69+
unsafe {
70+
if let Some(orig) = hook_iat(api.game_base(), WS2_32, "WSASendTo", hooked_sendto as *const ()) {
71+
let _ = ORIG_SENDTO.set(std::mem::transmute::<*const (), WsaSendToFn>(orig));
72+
} else {
73+
api.log_warn("national match: WSASendTo import not found");
74+
}
75+
76+
if let Some(orig) = hook_iat(api.game_base(), WS2_32, "WSARecvFrom", hooked_recvfrom as *const ()) {
77+
let _ = ORIG_RECVFROM.set(std::mem::transmute::<*const (), WsaRecvFromFn>(orig));
78+
} else {
79+
api.log_warn("national match: WSARecvFrom import not found");
80+
}
81+
}
82+
83+
api.log_info("national match: enabled (UDP<->TCP relay)");
84+
}
85+
86+
unsafe fn collect_payload(buffers: *const WSABUF, count: u32) -> Vec<u8> {
87+
let mut data = Vec::new();
88+
for i in 0..count {
89+
let buf = &*buffers.add(i as usize);
90+
if !buf.buf.is_null() && buf.len > 0 {
91+
data.extend_from_slice(std::slice::from_raw_parts(buf.buf, buf.len as usize));
92+
}
93+
}
94+
data
95+
}
96+
97+
fn sockaddr_ipv4(addr: *const SOCKADDR) -> Option<([u8; 4], u16)> {
98+
if addr.is_null() {
99+
return None;
100+
}
101+
unsafe {
102+
let sin = &*(addr as *const SOCKADDR_IN);
103+
let ip = sin.sin_addr.S_un.S_addr.to_ne_bytes();
104+
let port = u16::from_be(sin.sin_port);
105+
Some((ip, port))
106+
}
107+
}
108+
109+
fn build_frame(packet_type: u8, payload: &[u8]) -> Vec<u8> {
110+
let len = payload.len();
111+
let mut frame = Vec::with_capacity(3 + len);
112+
frame.push(packet_type);
113+
frame.push((len & 0xFF) as u8);
114+
frame.push(((len >> 8) & 0xFF) as u8);
115+
frame.extend_from_slice(payload);
116+
frame
117+
}
118+
119+
fn send_to_reflector(frame: &[u8]) -> bool {
120+
use std::io::Write;
121+
let Some(state) = STATE.get() else {
122+
return false;
123+
};
124+
125+
let mut guard = state.reflector.lock().unwrap();
126+
if guard.is_none() {
127+
let addr = state.reflector_addr.lock().unwrap();
128+
let port = *state.reflector_port.lock().unwrap();
129+
if let Some(ip) = *addr {
130+
let target = std::net::SocketAddr::from((ip, port));
131+
if let Ok(stream) = TcpStream::connect(target) {
132+
let _ = stream.set_nodelay(true);
133+
*guard = Some(stream);
134+
}
135+
}
136+
}
137+
138+
if let Some(stream) = guard.as_mut() {
139+
if stream.write_all(frame).is_ok() {
140+
return true;
141+
}
142+
}
143+
*guard = None;
144+
false
145+
}
146+
147+
unsafe extern "system" fn hooked_sendto(
148+
socket: usize,
149+
buffers: *const WSABUF,
150+
buffer_count: u32,
151+
bytes_sent: *mut u32,
152+
flags: u32,
153+
to: *const SOCKADDR,
154+
to_len: i32,
155+
overlapped: *mut c_void,
156+
completion: *mut c_void,
157+
) -> i32 {
158+
let Some(state) = STATE.get() else {
159+
return passthrough_sendto(
160+
socket, buffers, buffer_count, bytes_sent, flags, to, to_len, overlapped, completion,
161+
);
162+
};
163+
164+
let payload = collect_payload(buffers, buffer_count);
165+
let dest = sockaddr_ipv4(to);
166+
167+
let is_holdpunch = payload.len() >= 4 && &payload[0..4] == b"{\"ro";
168+
169+
if is_holdpunch {
170+
if let Some((ip, port)) = dest {
171+
*state.reflector_addr.lock().unwrap() = Some(ip);
172+
*state.reflector_port.lock().unwrap() = port;
173+
}
174+
let frame = build_frame(TYPE_HOLDPUNCH, &payload);
175+
send_to_reflector(&frame);
176+
177+
let already = {
178+
let mut sent = state.music_sent.lock().unwrap();
179+
let was = *sent;
180+
*sent = true;
181+
was
182+
};
183+
if !already {
184+
let music = build_frame(TYPE_MUSIC, &[0, 0]);
185+
send_to_reflector(&music);
186+
}
187+
188+
report_sent(bytes_sent, payload.len());
189+
return 0;
190+
}
191+
192+
let reflector_ip = *state.reflector_addr.lock().unwrap();
193+
if let (Some((ip, port)), Some(refl)) = (dest, reflector_ip) {
194+
let frame = if ip == refl {
195+
build_frame(TYPE_REFLECTOR, &payload)
196+
} else {
197+
let mut tunnel = Vec::with_capacity(6 + payload.len());
198+
tunnel.extend_from_slice(&ip);
199+
tunnel.extend_from_slice(&port.to_be_bytes());
200+
tunnel.extend_from_slice(&payload);
201+
build_frame(TYPE_TUNNEL, &tunnel)
202+
};
203+
send_to_reflector(&frame);
204+
report_sent(bytes_sent, payload.len());
205+
return 0;
206+
}
207+
208+
passthrough_sendto(
209+
socket, buffers, buffer_count, bytes_sent, flags, to, to_len, overlapped, completion,
210+
)
211+
}
212+
213+
unsafe fn report_sent(bytes_sent: *mut u32, len: usize) {
214+
if !bytes_sent.is_null() {
215+
*bytes_sent = len as u32;
216+
}
217+
}
218+
219+
unsafe fn passthrough_sendto(
220+
socket: usize,
221+
buffers: *const WSABUF,
222+
buffer_count: u32,
223+
bytes_sent: *mut u32,
224+
flags: u32,
225+
to: *const SOCKADDR,
226+
to_len: i32,
227+
overlapped: *mut c_void,
228+
completion: *mut c_void,
229+
) -> i32 {
230+
if let Some(orig) = ORIG_SENDTO.get() {
231+
return orig(
232+
socket, buffers, buffer_count, bytes_sent, flags, to, to_len, overlapped, completion,
233+
);
234+
}
235+
0
236+
}
237+
238+
unsafe extern "system" fn hooked_recvfrom(
239+
socket: usize,
240+
buffers: *const WSABUF,
241+
buffer_count: u32,
242+
bytes_recvd: *mut u32,
243+
flags: *mut u32,
244+
from: *mut SOCKADDR,
245+
from_len: *mut i32,
246+
overlapped: *mut c_void,
247+
completion: *mut c_void,
248+
) -> i32 {
249+
let Some(state) = STATE.get() else {
250+
return passthrough_recvfrom(
251+
socket, buffers, buffer_count, bytes_recvd, flags, from, from_len, overlapped,
252+
completion,
253+
);
254+
};
255+
256+
let frame = match recv_frame(state) {
257+
Some(f) => f,
258+
None => {
259+
return passthrough_recvfrom(
260+
socket, buffers, buffer_count, bytes_recvd, flags, from, from_len, overlapped,
261+
completion,
262+
);
263+
}
264+
};
265+
266+
let (packet_type, payload) = frame;
267+
let reflector_ip = state.reflector_addr.lock().unwrap().unwrap_or([0, 0, 0, 0]);
268+
let reflector_port = *state.reflector_port.lock().unwrap();
269+
270+
match packet_type {
271+
TYPE_TUNNEL if payload.len() >= 6 => {
272+
let ip = [payload[0], payload[1], payload[2], payload[3]];
273+
let port = u16::from_be_bytes([payload[4], payload[5]]);
274+
write_from(from, from_len, ip, port);
275+
deliver(buffers, buffer_count, bytes_recvd, &payload[6..]);
276+
}
277+
TYPE_MUSIC => {
278+
write_from(from, from_len, reflector_ip, reflector_port);
279+
deliver(buffers, buffer_count, bytes_recvd, ISFINISH_FALSE);
280+
}
281+
_ => {
282+
write_from(from, from_len, reflector_ip, reflector_port);
283+
deliver(buffers, buffer_count, bytes_recvd, &payload);
284+
}
285+
}
286+
287+
0
288+
}
289+
290+
fn recv_frame(state: &State) -> Option<(u8, Vec<u8>)> {
291+
use std::io::Read;
292+
let mut guard = state.reflector.lock().unwrap();
293+
let stream = guard.as_mut()?;
294+
295+
let mut header = [0u8; 3];
296+
if stream.read_exact(&mut header).is_err() {
297+
*guard = None;
298+
return None;
299+
}
300+
301+
let packet_type = header[0];
302+
let len = (header[1] as usize) | ((header[2] as usize) << 8);
303+
let mut payload = vec![0u8; len];
304+
if len > 0 && stream.read_exact(&mut payload).is_err() {
305+
*guard = None;
306+
return None;
307+
}
308+
Some((packet_type, payload))
309+
}
310+
311+
unsafe fn write_from(from: *mut SOCKADDR, from_len: *mut i32, ip: [u8; 4], port: u16) {
312+
if from.is_null() {
313+
return;
314+
}
315+
let sin = &mut *(from as *mut SOCKADDR_IN);
316+
sin.sin_family = windows_sys::Win32::Networking::WinSock::AF_INET;
317+
sin.sin_port = port.to_be();
318+
sin.sin_addr.S_un.S_addr = u32::from_ne_bytes(ip);
319+
if !from_len.is_null() {
320+
*from_len = std::mem::size_of::<SOCKADDR_IN>() as i32;
321+
}
322+
}
323+
324+
unsafe fn deliver(buffers: *const WSABUF, buffer_count: u32, bytes_recvd: *mut u32, data: &[u8]) {
325+
let mut written = 0usize;
326+
for i in 0..buffer_count {
327+
if written >= data.len() {
328+
break;
329+
}
330+
let buf = &*buffers.add(i as usize);
331+
if buf.buf.is_null() || buf.len == 0 {
332+
continue;
333+
}
334+
let take = std::cmp::min(buf.len as usize, data.len() - written);
335+
std::ptr::copy_nonoverlapping(data.as_ptr().add(written), buf.buf, take);
336+
written += take;
337+
}
338+
if !bytes_recvd.is_null() {
339+
*bytes_recvd = written as u32;
340+
}
341+
}
342+
343+
unsafe fn passthrough_recvfrom(
344+
socket: usize,
345+
buffers: *const WSABUF,
346+
buffer_count: u32,
347+
bytes_recvd: *mut u32,
348+
flags: *mut u32,
349+
from: *mut SOCKADDR,
350+
from_len: *mut i32,
351+
overlapped: *mut c_void,
352+
completion: *mut c_void,
353+
) -> i32 {
354+
if let Some(orig) = ORIG_RECVFROM.get() {
355+
return orig(
356+
socket, buffers, buffer_count, bytes_recvd, flags, from, from_len, overlapped,
357+
completion,
358+
);
359+
}
360+
-1
361+
}

0 commit comments

Comments
 (0)