Skip to main content

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}