bairelay_neolink_core/lib.rs
1#![warn(unused_crate_dependencies)]
2#![warn(missing_docs)]
3//! # Neolink-Core
4//!
5//! Neolink-Core is a rust library for interacting with reolink and family cameras.
6//!
7//! Most high level camera controls are in the [`bc_protocol`] module
8//!
9//! A camera can be initialised with
10//!
11//! ```no_run
12//! # tokio::runtime::Runtime::new().unwrap().block_on(async {
13//! use bairelay_neolink_core::bc_protocol::{BcCamera, BcCameraOpt, DiscoveryMethods, ConnectionProtocol, Credentials};
14//! let options = BcCameraOpt {
15//! name: "CamName".to_string(),
16//! channel_id: 0,
17//! addrs: ["192.168.1.1".parse().unwrap()].to_vec(),
18//! port: Some(9000),
19//! uid: Some("CAMUID".to_string()),
20//! protocol: ConnectionProtocol::TcpUdp,
21//! discovery: DiscoveryMethods::Relay,
22//! credentials: Credentials {
23//! username: "username".to_string(),
24//! password: Some("password".to_string()),
25//! },
26//! debug: false,
27//! max_discovery_retries: 10,
28//! };
29//! let mut camera = BcCamera::new(&options).await.unwrap();
30//! # })
31//! ```
32//!
33//! After that login can be conducted with
34//!
35//! ```no_run
36//! # tokio::runtime::Runtime::new().unwrap().block_on(async {
37//! # use bairelay_neolink_core::bc_protocol::{BcCamera, BcCameraOpt, DiscoveryMethods, ConnectionProtocol, Credentials};
38//! # let options = BcCameraOpt {
39//! # name: "CamName".to_string(),
40//! # channel_id: 0,
41//! # addrs: ["192.168.1.1".parse().unwrap()].to_vec(),
42//! # port: Some(9000),
43//! # uid: Some("CAMUID".to_string()),
44//! # protocol: ConnectionProtocol::TcpUdp,
45//! # discovery: DiscoveryMethods::Relay,
46//! # credentials: Credentials {
47//! # username: "username".to_string(),
48//! # password: Some("password".to_string()),
49//! # },
50//! # debug: false,
51//! # max_discovery_retries: 10,
52//! # };
53//! # let mut camera = BcCamera::new(&options).await.unwrap();
54//! camera.login().await;
55//! # })
56//! ```
57//! For further commands see the [`bc_protocol::BcCamera`] struct.
58//!
59
60/// Contains low level BC structures and formats
61pub mod bc;
62/// Contains high level interfaces for the camera
63pub mod bc_protocol;
64/// Contains low level structures and formats for the media substream
65pub mod bcmedia;
66/// Contains low level structures and formats for the udpstream
67pub mod bcudp;
68
69/// This is the top level error structure of the library
70///
71/// Most commands will either return their `Ok(result)` or this `Err(Error)`
72pub use bc_protocol::Error;
73
74pub(crate) use bc_protocol::{Credentials, Result};
75
76pub(crate) type NomErrorType<'a> = nom::error::VerboseError<&'a [u8]>;
77
78/// Thin public shims around otherwise `pub(crate)` parsers so the
79/// out-of-tree fuzz harness can drive them. Gated on the `fuzz-api`
80/// Cargo feature.
81#[cfg(feature = "fuzz-api")]
82pub mod fuzz_api {
83 use bytes::BytesMut;
84
85 /// Drive `Bc::deserialize` on arbitrary input under the
86 /// `Unencrypted` codec context.
87 pub fn parse_bc(input: &[u8]) -> Result<crate::bc::model::Bc, crate::Error> {
88 let ctx = crate::bc::model::BcContext::new_with_encryption(
89 crate::bc::crypto::EncryptionProtocol::Unencrypted,
90 );
91 let mut buf = BytesMut::from(input);
92 crate::bc::model::Bc::deserialize(&ctx, &mut buf)
93 }
94
95 /// Drive `BcXml::try_parse` on arbitrary input.
96 pub fn parse_bc_xml(input: &[u8]) -> Result<crate::bc::xml::BcXml, quick_xml::de::DeError> {
97 crate::bc::xml::BcXml::try_parse(input)
98 }
99
100 pub use crate::bc_protocol::connection::udpsource::{UdpFlowState, REORDER_CAP};
101 pub use crate::bcudp::model::{UdpAck, UdpData};
102
103 /// Drive a sequence of `UdpFlowState` operations parsed from
104 /// arbitrary bytes. Asserts the bounded-state invariants
105 /// (REORDER_CAP cap on `received`, no panic on u32::MAX corners)
106 /// survive every input.
107 ///
108 /// Encoding: each op consumes 1 tag byte + payload:
109 ///
110 /// - tag % 4 == 0: handle_data — 5 bytes (4 = packet_id LE, 1 =
111 /// payload-length-cap byte). The synthetic UdpData payload is
112 /// filled with the tag value to keep the fuzz corpus dense.
113 /// - tag % 4 == 1: handle_ack — 5 bytes (4 = ack.packet_id LE,
114 /// 1 = ack.payload length, capped at remaining input).
115 /// - tag % 4 == 2: enqueue_send — 1 byte (length of synthetic
116 /// payload, capped at remaining input).
117 /// - tag % 4 == 3: drain_contiguous + build_send_ack — no params.
118 ///
119 /// Input length is clamped to 8 KiB so the harness drives at most
120 /// ~1 k operations per iteration — `sent` is unbounded by design
121 /// (the camera-side ack stream shrinks it in production but the
122 /// fuzzer doesn't model that), so a multi-MB input would OOM the
123 /// process with synthetic enqueue_send growth that has nothing to
124 /// do with real bug-finding.
125 pub fn flow_state_drive_arbitrary(data: &[u8]) {
126 let data = &data[..data.len().min(8 * 1024)];
127 let mut s = UdpFlowState::new(0xABCD, 0x1234);
128 let mut p = 0;
129 while p < data.len() {
130 let tag = data[p];
131 p += 1;
132 match tag % 4 {
133 0 => {
134 if data.len() - p < 5 {
135 break;
136 }
137 let packet_id =
138 u32::from_le_bytes([data[p], data[p + 1], data[p + 2], data[p + 3]]);
139 let payload_len = data[p + 4] as usize;
140 p += 5;
141 let _ = s.handle_data(UdpData {
142 connection_id: 0xABCD,
143 packet_id,
144 payload: vec![tag; payload_len.min(64)],
145 });
146 }
147 1 => {
148 if data.len() - p < 5 {
149 break;
150 }
151 let pkt = u32::from_le_bytes([data[p], data[p + 1], data[p + 2], data[p + 3]]);
152 let plen = data[p + 4] as usize;
153 p += 5;
154 let payload_len = plen.min(data.len().saturating_sub(p));
155 let payload: Vec<u8> = data[p..p + payload_len].to_vec();
156 p += payload_len;
157 s.handle_ack(UdpAck {
158 connection_id: 0x1234,
159 packet_id: pkt,
160 group_id: 0,
161 maybe_latency: 0,
162 payload,
163 });
164 }
165 2 => {
166 if data.len() - p < 1 {
167 break;
168 }
169 let n = data[p] as usize;
170 p += 1;
171 let take = n.min(data.len().saturating_sub(p)).min(4096);
172 let _ = s.enqueue_send(&data[p..p + take]);
173 p += take;
174 }
175 _ => {
176 let _ = s.drain_contiguous();
177 let _ = s.build_send_ack();
178 }
179 }
180 // Bounded-state invariants — these are the load-bearing
181 // claims the production code makes. Crash → fuzzer flag.
182 assert!(s.received_len() <= REORDER_CAP);
183 }
184 }
185}
186
187/// Offline decoder primitives for captured BcUdp sessions. Drives
188/// Bc / BcUdp parsing + AES-CFB decryption against a pcap recorded with
189/// tcpdump. Consumed by `tests/scripts/decode-bc-pcap`. Gated on the
190/// `pcap-decode-api` Cargo feature so production builds never compile
191/// this surface.
192///
193/// Stream model: a captured BcUdp session has two directions
194/// (client→camera and camera→client). Each direction reassembles its own
195/// sequence of `UdpData` packets by `packet_id` into a Bc TCP-like byte
196/// stream, then drives `Bc::deserialize` against it. Both directions
197/// share a single `BcContext` whose `EncryptionProtocol` is updated when
198/// the camera's login reply (msg_id=1, `response_code >> 8 == 0xdd`)
199/// surfaces — same negotiation logic the production codex runs, lifted
200/// into `Session::feed_datagram`.
201#[cfg(feature = "pcap-decode-api")]
202pub mod pcap_decode_api {
203 use bytes::BytesMut;
204 use std::collections::BTreeMap;
205
206 use crate::bc::crypto::EncryptionProtocol;
207 use crate::bc::model::{Bc, BcBody, BcContext, BcMeta, ModernMsg};
208 use crate::bc::xml::{BcPayloads, BcXml, Encryption};
209 use crate::bcudp::model::BcUdp;
210
211 pub use crate::bc::model::{Bc as BcMessage, BcMeta as BcMessageMeta};
212 pub use crate::bc::xml::BcXml as DecodedXml;
213 pub use crate::bc_protocol::Credentials;
214 pub use crate::bcudp::model::{UdpAck, UdpData, UdpDiscovery};
215 pub use crate::Error;
216
217 /// Source of a UDP datagram in a captured session.
218 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
219 pub enum Direction {
220 /// Client to camera (operator software → camera).
221 ClientToCamera,
222 /// Camera to client (camera → operator software).
223 CameraToClient,
224 }
225
226 /// Per-direction reassembly state: pending out-of-order `UdpData`
227 /// packets keyed by `packet_id`, plus the contiguous Bc byte stream
228 /// drained from them.
229 struct DirState {
230 next_packet_id: Option<u32>,
231 pending: BTreeMap<u32, Vec<u8>>,
232 bc_buf: BytesMut,
233 }
234
235 impl DirState {
236 fn new() -> Self {
237 Self {
238 next_packet_id: None,
239 pending: BTreeMap::new(),
240 bc_buf: BytesMut::new(),
241 }
242 }
243
244 fn feed(&mut self, data: UdpData) {
245 let id = data.packet_id;
246 // First packet seen sets the baseline. Production cameras
247 // don't always start at 0 (depends on the connection negotiation
248 // instant); align the cursor to whatever the first observed
249 // packet_id is so we don't stall waiting for "missing" earlier
250 // packets that simply weren't captured.
251 let next = *self.next_packet_id.get_or_insert(id);
252 if id < next {
253 return; // late duplicate / retransmit, ignore
254 }
255 self.pending.insert(id, data.payload);
256 while let Some(payload) = self.pending.remove(&self.next_packet_id.unwrap()) {
257 self.bc_buf.extend_from_slice(&payload);
258 let cur = self.next_packet_id.unwrap();
259 self.next_packet_id = Some(cur.wrapping_add(1));
260 }
261 }
262 }
263
264 /// One captured BcUdp session. Holds the shared encryption state +
265 /// per-direction reassembly buffers.
266 pub struct Session {
267 ctx: BcContext,
268 c2d: DirState,
269 d2c: DirState,
270 }
271
272 /// Result of feeding one datagram: zero or more decoded Bc messages
273 /// that became complete after the new bytes arrived.
274 #[derive(Debug)]
275 pub struct DecodedMessage {
276 /// Direction the message travelled.
277 pub direction: Direction,
278 /// The decoded Bc message (header + decrypted XML / binary body).
279 pub bc: BcMessage,
280 /// Best-effort plaintext view of a binary payload when the camera
281 /// is in `FullAes` mode and the underlying decoder returned the
282 /// raw wire bytes (no `<encryptLen>` was present, so the
283 /// production codec couldn't tell whether the bytes were
284 /// already plaintext or still ciphertext). Some Bc messages —
285 /// notably control replies like `MSG_ID_GET_DST` — encrypt the
286 /// payload on the wire even without an `<encryptLen>` marker;
287 /// others — notably the high-throughput stream chunks
288 /// (`MSG_ID_VIDEO`) — leave the payload plaintext on the wire
289 /// even in `FullAes` mode. Production code never needed to tell
290 /// these apart because bairelay's own commands always include
291 /// `<encryptLen>` when relevant; offline decoders facing
292 /// arbitrary captured client traffic do.
293 ///
294 /// The tool consuming this struct prints the raw bytes from
295 /// `bc.body` as a hexdump and additionally checks
296 /// `manually_decrypted_binary` for an XML / UTF-8 view —
297 /// whichever is meaningful is what the operator wants to see.
298 /// `None` when the Bc body is not `Binary` or the context isn't
299 /// `FullAes`.
300 pub manually_decrypted_binary: Option<Vec<u8>>,
301 }
302
303 impl Session {
304 /// Construct a session decoder for the given camera credentials.
305 /// The `Credentials` value is used to derive the AES key once the
306 /// login response selects an AES variant.
307 pub fn new(creds: Credentials) -> Self {
308 let mut ctx = BcContext::new(creds);
309 // Enable BcCodex's plaintext-payload trace prints so the
310 // caller's `log` subscriber can surface raw decrypted XML —
311 // including fields the `BcXml` struct doesn't model, which
312 // serde silently drops on parse. This is the only way to
313 // see e.g. `<Dst>` blocks inside an unknown msg_id reply.
314 ctx.debug_on();
315 Self {
316 ctx,
317 c2d: DirState::new(),
318 d2c: DirState::new(),
319 }
320 }
321
322 /// Feed one captured UDP datagram payload. Discovery and Ack
323 /// packets are recognised but not surfaced (they don't carry Bc
324 /// messages). Data packets reassemble per-direction; for every
325 /// complete Bc message that becomes decodable from the new bytes,
326 /// `on_msg` is called in arrival order — once per message,
327 /// before the next is decoded. The callback shape is critical
328 /// for tools that capture bairelay_neolink_core's `log::trace!` output
329 /// to attach raw decrypted payloads to specific messages: the
330 /// trace channel is a shared global, so the caller must drain
331 /// it between successive decodes.
332 pub fn feed_datagram<F>(
333 &mut self,
334 direction: Direction,
335 datagram: &[u8],
336 mut on_msg: F,
337 ) -> Result<(), Error>
338 where
339 F: FnMut(DecodedMessage),
340 {
341 let mut buf = BytesMut::from(datagram);
342 let bcudp = match BcUdp::deserialize(&mut buf) {
343 Ok(b) => b,
344 Err(Error::NomIncomplete(_)) => return Ok(()),
345 Err(e) => return Err(e),
346 };
347
348 let dir_state = match direction {
349 Direction::ClientToCamera => &mut self.c2d,
350 Direction::CameraToClient => &mut self.d2c,
351 };
352
353 match bcudp {
354 BcUdp::Data(data) => {
355 dir_state.feed(data);
356 loop {
357 match Bc::deserialize(&self.ctx, &mut dir_state.bc_buf) {
358 Ok(bc) => {
359 // Mirror the BcCodex login-response
360 // negotiation logic — without this the
361 // follow-on messages don't decrypt.
362 if let Bc {
363 meta:
364 BcMeta {
365 msg_id: 1,
366 response_code,
367 ..
368 },
369 body:
370 BcBody::ModernMsg(ModernMsg {
371 payload:
372 Some(BcPayloads::BcXml(BcXml {
373 encryption: Some(Encryption { ref nonce, .. }),
374 ..
375 })),
376 ..
377 }),
378 } = bc
379 {
380 if response_code >> 8 == 0xdd {
381 let kind = (response_code & 0xff) as u8;
382 let new_proto = match kind {
383 0x00 => EncryptionProtocol::Unencrypted,
384 0x01 => EncryptionProtocol::BCEncrypt,
385 0x02 => EncryptionProtocol::aes(
386 self.ctx.credentials.make_aeskey(nonce),
387 ),
388 0x12 => EncryptionProtocol::full_aes(
389 self.ctx.credentials.make_aeskey(nonce),
390 ),
391 other => {
392 return Err(Error::UnknownEncryption(
393 other as usize,
394 ));
395 }
396 };
397 self.ctx.set_encrypted(new_proto);
398 }
399 }
400
401 // Mirror BcCodex's binary-mode bookkeeping
402 // so streaming msg_nums (3 / 4) don't get
403 // mis-parsed as XML on subsequent packets.
404 if let BcBody::ModernMsg(ModernMsg {
405 extension:
406 Some(crate::bc::xml::Extension {
407 binary_data: Some(on_off),
408 ..
409 }),
410 ..
411 }) = bc.body
412 {
413 if on_off == 0 {
414 self.ctx.binary_off(bc.meta.msg_num);
415 } else {
416 self.ctx.binary_on(bc.meta.msg_num);
417 }
418 }
419
420 // Compute a "would-be plaintext" view for binary
421 // payloads when the session is in FullAes —
422 // see DecodedMessage's field doc for why.
423 let manually_decrypted_binary =
424 match (&self.ctx.encryption_protocol, &bc.body) {
425 (
426 EncryptionProtocol::FullAes { .. },
427 BcBody::ModernMsg(ModernMsg {
428 payload: Some(BcPayloads::Binary(bytes)),
429 ..
430 }),
431 ) => {
432 Some(self.ctx.encryption_protocol.decrypt(
433 bc.meta.channel_id as u32,
434 bytes.as_slice(),
435 ))
436 }
437 _ => None,
438 };
439 on_msg(DecodedMessage {
440 direction,
441 bc,
442 manually_decrypted_binary,
443 });
444 }
445 Err(Error::NomIncomplete(_)) => break,
446 Err(e) => return Err(e),
447 }
448 }
449 }
450 BcUdp::Discovery(_) | BcUdp::Ack(_) => {
451 // These don't carry Bc messages; ignore for the
452 // reassembled-stream view. (Discovery payloads are
453 // XOR-encrypted XML and worth rendering separately
454 // if needed; today's caller only wants Bc traffic.)
455 }
456 }
457 Ok(())
458 }
459 }
460}