minetest_protocol/wire/
audit.rs

1//! Audit
2//!
3//! When auditing is enabled, every deserialized Packet or Command is immediately
4//! re-serialized, and the results compared byte-by-byte. Any difference is a
5//! fatal error.
6//!
7//! This is useful during development, to verify that new ser/deser methods are correct.
8//!
9//! But it should not be enabled normally, because a malformed packet from a
10//! broken/modified client will cause a crash.
11
12use anyhow::bail;
13use anyhow::Result;
14
15use super::command::serialize_commandref;
16use super::command::CommandRef;
17use super::command::ToClientCommand;
18use super::ser::VecSerializer;
19use super::types::ProtocolContext;
20use super::util::decompress_zlib;
21use super::util::zstd_decompress;
22use std::sync::atomic::AtomicBool;
23
24static AUDIT_ENABLED: AtomicBool = AtomicBool::new(false);
25
26pub fn audit_on() {
27    AUDIT_ENABLED.store(true, std::sync::atomic::Ordering::SeqCst);
28}
29
30pub fn audit_command<Cmd: CommandRef>(context: ProtocolContext, orig: &[u8], command: &Cmd) {
31    if !AUDIT_ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
32        return;
33    }
34    let mut ser = VecSerializer::new(context, 2 * orig.len());
35    match serialize_commandref(command, &mut ser) {
36        Ok(_) => (),
37        Err(err) => {
38            println!("AUDIT: Reserialization failed");
39            println!("AUDIT: ORIGINAL = {:?}", orig);
40            println!("AUDIT: PARSED = {:?}", command);
41            println!("ERR = {:?}", err);
42            std::process::exit(1);
43        }
44    }
45    let reser = ser.take();
46    let reser = reser.as_slice();
47    match audit_command_inner(context, orig, reser, command) {
48        Ok(_) => (),
49        Err(err) => {
50            println!("AUDIT: Unknown error occurred auditing of command");
51            println!("AUDIT: PARSED = {:?}", command);
52            println!("AUDIT: ORIGINAL     = {:?}", orig);
53            println!("AUDIT: RESERIALIZED = {:?}", reser);
54            println!("ERR = {:?}", err);
55            std::process::exit(1);
56        }
57    }
58}
59
60fn audit_command_inner<Cmd: CommandRef>(
61    context: ProtocolContext,
62    orig: &[u8],
63    reser: &[u8],
64    command: &Cmd,
65) -> Result<()> {
66    // zstd or zlib re-compression is not guaranteed to be the same,
67    // so handle these separately.
68    match command.toclient_ref() {
69        Some(ToClientCommand::Blockdata(_)) => {
70            if context.ser_fmt >= 29 {
71                // Layout in format 29 and above:
72                //
73                //   command type: u16
74                //   pos: v3s16, (6 bytes)
75                //   datastring: ZStdCompressed<MapBlock>,
76                //   network_specific_version: u8
77                do_compare(
78                    "BlockData prefix (ver>=29)",
79                    &reser[..8],
80                    &orig[..8],
81                    command,
82                );
83                do_compare(
84                    "BlockData suffix (ver>=29)",
85                    &reser[reser.len() - 1..reser.len()],
86                    &orig[orig.len() - 1..orig.len()],
87                    command,
88                );
89                let reser = zstd_decompress_to_vec(&reser[8..reser.len() - 1])?;
90                let orig = zstd_decompress_to_vec(&orig[8..orig.len() - 1])?;
91                do_compare("Blockdata contents (ver>=29)", &reser, &orig, command);
92            } else {
93                // Layout in ver 28:
94                //
95                //   command type: u16         (2 bytes)
96                //   pos: v3s16                (6 bytes)
97                //   flags: u8                 (1 byte)
98                //   lighting_complete: u16    (2 bytes)
99                //   content_width: u8         (1 byte)
100                //   param_width: u8           (1 byte)
101                //   nodes: ZLibCompressed     (var size)
102                //   metadata: ZLibCompressed  (var size)
103                //   network_specific_version  (1 byte)
104                do_compare(
105                    "BlockData prefix (ver==28)",
106                    &reser[..13],
107                    &orig[..13],
108                    command,
109                );
110                do_compare(
111                    "BlockData suffix (ver==28)",
112                    &reser[reser.len() - 1..],
113                    &orig[orig.len() - 1..],
114                    command,
115                );
116
117                let reser_contents = {
118                    let (consumed1, nodes_raw) = decompress_zlib(&reser[13..])?;
119                    let (consumed2, metadata_raw) = decompress_zlib(&reser[13 + consumed1..])?;
120                    if 13 + consumed1 + consumed2 + 1 != reser.len() {
121                        bail!("Reserialized command does not have the right size")
122                    }
123                    (nodes_raw, metadata_raw)
124                };
125                let orig_contents = {
126                    let (consumed1, nodes_raw) = decompress_zlib(&orig[13..])?;
127                    let (consumed2, metadata_raw) = decompress_zlib(&orig[13 + consumed1..])?;
128                    if 13 + consumed1 + consumed2 + 1 != orig.len() {
129                        bail!("Original command does not seem to have the right size")
130                    }
131                    (nodes_raw, metadata_raw)
132                };
133                do_compare(
134                    "Uncompressed nodes (ver 28)",
135                    &reser_contents.0,
136                    &orig_contents.0,
137                    command,
138                );
139                do_compare(
140                    "Uncompressed node metadata (ver 28)",
141                    &reser_contents.1,
142                    &orig_contents.1,
143                    command,
144                );
145            }
146        }
147        Some(ToClientCommand::NodemetaChanged(_))
148        | Some(ToClientCommand::Itemdef(_))
149        | Some(ToClientCommand::Nodedef(_)) => {
150            // These contain a single zlib-compressed value.
151            // The prefix is a u16 command type, followed by u32 zlib size.
152            let reser = zlib_decompress_to_vec(&reser[6..]);
153            let orig = zlib_decompress_to_vec(&orig[6..]);
154            do_compare("zlib decompressed body", &reser, &orig, command);
155        }
156        _ => {
157            do_compare("default", reser, orig, command);
158        }
159    };
160    Ok(())
161}
162
163fn do_compare<Cmd: CommandRef>(what: &str, reser: &[u8], orig: &[u8], command: &Cmd) {
164    if reser != orig {
165        println!(
166            "AUDIT: Mismatch between original and re-serialized ({})",
167            what
168        );
169        println!("AUDIT: ORIGINAL     = {:?}", orig);
170        println!("AUDIT: RESERIALIZED = {:?}", reser);
171        println!("AUDIT: PARSED = {:?}", command);
172        std::process::exit(1);
173    }
174}
175
176fn zlib_decompress_to_vec(compressed: &[u8]) -> Vec<u8> {
177    match miniz_oxide::inflate::decompress_to_vec_zlib(compressed) {
178        Ok(uncompressed) => uncompressed,
179        Err(_) => {
180            println!("AUDIT: Decompression failed unexpectedly");
181            std::process::exit(1);
182        }
183    }
184}
185
186fn zstd_decompress_to_vec(compressed: &[u8]) -> Result<Vec<u8>> {
187    let mut result = Vec::new();
188    zstd_decompress(compressed, |chunk| {
189        result.extend(chunk);
190        Ok(())
191    })?;
192    Ok(result)
193}