use super::*;
use crate::database::ObjectDatabase;
use crate::property_metadata::PropertyWriteCapability;
use bacnet_encoding::constructed::{decode_host_n_port, encode_host_n_port};
use bacnet_types::bip_port::{BbmdChanges, BbmdTables, BipPortMode};
use bacnet_types::constructed::{
BACnetBDTEntry, BACnetFDTEntry, BACnetHostAddress, BACnetHostNPort,
};
use bacnet_types::enums::{ErrorClass, ErrorCode, PropertyIdentifier as P};
use bacnet_types::error::RemoteError;
use bytes::BytesMut;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::{Arc, Mutex};
const UNSET: BACnetHostNPort = BACnetHostNPort {
host: BACnetHostAddress::None,
port: 0,
};
fn global() -> BACnetHostNPort {
BACnetHostNPort {
host: BACnetHostAddress::Ip(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1))),
port: 47809,
}
}
struct Tables {
nat: Mutex<bool>,
global: Mutex<BACnetHostNPort>,
capable: bool,
activated: Mutex<Vec<BbmdChanges>>,
}
impl Tables {
fn new(capable: bool) -> Arc<Self> {
Arc::new(Self {
nat: Mutex::new(false),
global: Mutex::new(UNSET),
capable,
activated: Mutex::default(),
})
}
}
fn refusal() -> Error {
Error::protocol(ErrorClass::PROPERTY, ErrorCode::VALUE_OUT_OF_RANGE)
}
impl BbmdTables for Tables {
fn broadcast_distribution_table(&self) -> Vec<BACnetBDTEntry> {
Vec::new()
}
fn accepts_foreign_device_registrations(&self) -> bool {
false
}
fn foreign_device_table(&self) -> Vec<BACnetFDTEntry> {
Vec::new()
}
fn check_broadcast_distribution_table(&self, _: &[BACnetBDTEntry]) -> Result<(), Error> {
Ok(())
}
fn nat_traversal(&self) -> Option<bool> {
self.capable.then(|| *self.nat.lock().unwrap())
}
fn global_address(&self) -> Option<BACnetHostNPort> {
self.capable.then(|| self.global.lock().unwrap().clone())
}
fn check_value(&self, changes: &BbmdChanges) -> Result<(), Error> {
match changes.global_address.as_ref().map(|g| (&g.host, g.port)) {
None | Some((BACnetHostAddress::None, 0)) => Ok(()),
Some((BACnetHostAddress::Ip(IpAddr::V4(_)), port)) if port != 0 => Ok(()),
Some(_) => Err(refusal()),
}
}
fn check(&self, changes: &BbmdChanges) -> Result<(), Error> {
let global = changes
.global_address
.clone()
.unwrap_or_else(|| self.global.lock().unwrap().clone());
let usable = match (&global.host, global.port) {
(BACnetHostAddress::None, 0) => false,
(BACnetHostAddress::Ip(IpAddr::V4(_)), port) if port != 0 => true,
_ => return Err(refusal()),
};
let nat = changes.nat_traversal.unwrap_or(*self.nat.lock().unwrap());
if nat && !usable {
return Err(refusal());
}
Ok(())
}
fn activate(&self, changes: &BbmdChanges) -> Result<(), Error> {
self.check(changes)?;
if let Some(nat) = changes.nat_traversal {
*self.nat.lock().unwrap() = nat;
}
if let Some(global) = &changes.global_address {
*self.global.lock().unwrap() = global.clone();
}
self.activated.lock().unwrap().push(changes.clone());
Ok(())
}
fn can_change(&self) -> bool {
true
}
}
fn oid() -> ObjectIdentifier {
ObjectIdentifier::new(ObjectType::NETWORK_PORT, 1).unwrap()
}
fn bbmd_port(tables: &Arc<Tables>, activation: bool) -> (ObjectDatabase, Arc<()>) {
let mut db = ObjectDatabase::new();
let config = BipPortConfig {
ip_address: [127, 0, 0, 1],
..Default::default()
};
db.add(Box::new(
NetworkPortObject::new_bip(1, "NP", config).unwrap(),
))
.unwrap();
let (_, lease) = db
.reserve_bip_port_internal(oid(), [127, 0, 0, 1], 47808)
.unwrap();
let tables: Arc<dyn BbmdTables> = tables.clone();
let mode = BipPortMode::Bbmd {
tables: Some(tables),
};
db.publish_bip_port_internal(oid(), [127, 0, 0, 1], 47808, 1476, mode)
.unwrap();
db.set_network_port_activation_internal(oid(), activation)
.unwrap();
(db, lease)
}
fn read(db: &ObjectDatabase, property: P) -> Result<PropertyValue, Error> {
db.get(&oid()).unwrap().read_property(property, None)
}
fn global_read(db: &ObjectDatabase) -> BACnetHostNPort {
let Ok(PropertyValue::ApplicationData(octets)) = read(db, P::BACNET_IP_GLOBAL_ADDRESS) else {
panic!("an encoded BACnetHostNPort");
};
let (address, end) = decode_host_n_port(&octets, 0).unwrap();
assert_eq!(end, octets.len());
address
}
fn written(address: &BACnetHostNPort) -> PropertyValue {
let mut buf = BytesMut::new();
encode_host_n_port(&mut buf, address).unwrap();
PropertyValue::ApplicationData(buf.to_vec())
}
fn write(db: &mut ObjectDatabase, property: P, value: PropertyValue) -> Result<(), Error> {
db.get_mut(&oid())
.unwrap()
.write_property(property, None, value, None)
}
fn listed(db: &ObjectDatabase) -> Vec<P> {
db.get(&oid()).unwrap().property_list().to_vec()
}
fn is_unknown(result: Result<PropertyValue, Error>) -> bool {
matches!(result, Err(Error::Remote(RemoteError::Error { code, .. })) if code == ErrorCode::UNKNOWN_PROPERTY)
}
#[test]
fn a_nat_capable_ipv4_bbmd_serves_both_rows() {
let tables = Tables::new(true);
let (db, _lease) = bbmd_port(&tables, true);
let list = listed(&db);
assert!(list.contains(&P::BACNET_IP_NAT_TRAVERSAL));
assert!(list.contains(&P::BACNET_IP_GLOBAL_ADDRESS));
assert_eq!(
read(&db, P::BACNET_IP_NAT_TRAVERSAL).unwrap(),
PropertyValue::Boolean(false)
);
assert_eq!(global_read(&db), UNSET);
*tables.nat.lock().unwrap() = true;
*tables.global.lock().unwrap() = global();
assert_eq!(
read(&db, P::BACNET_IP_NAT_TRAVERSAL).unwrap(),
PropertyValue::Boolean(true)
);
assert_eq!(global_read(&db), global());
}
#[test]
fn a_bbmd_that_cant_run_behind_nat_has_neither_row() {
let tables = Tables::new(false);
let (mut db, _lease) = bbmd_port(&tables, true);
let list = listed(&db);
assert!(!list.contains(&P::BACNET_IP_NAT_TRAVERSAL));
assert!(!list.contains(&P::BACNET_IP_GLOBAL_ADDRESS));
assert!(is_unknown(read(&db, P::BACNET_IP_NAT_TRAVERSAL)));
assert!(is_unknown(read(&db, P::BACNET_IP_GLOBAL_ADDRESS)));
let error = write(
&mut db,
P::BACNET_IP_NAT_TRAVERSAL,
PropertyValue::Boolean(true),
);
assert!(
matches!(error, Err(Error::Remote(RemoteError::Error { code, .. })) if code == ErrorCode::UNKNOWN_PROPERTY),
"{error:?}"
);
}
#[test]
fn a_normal_port_has_neither_row() {
let mut db = ObjectDatabase::new();
db.add(Box::new(
NetworkPortObject::new_bip(1, "NP", BipPortConfig::default()).unwrap(),
))
.unwrap();
assert!(!listed(&db).contains(&P::BACNET_IP_NAT_TRAVERSAL));
assert!(is_unknown(read(&db, P::BACNET_IP_GLOBAL_ADDRESS)));
}
#[test]
fn written_nat_rows_read_back_and_wait_for_activation() {
let tables = Tables::new(true);
let (mut db, _lease) = bbmd_port(&tables, true);
for row in [P::BACNET_IP_NAT_TRAVERSAL, P::BACNET_IP_GLOBAL_ADDRESS] {
let capability = db
.get(&oid())
.unwrap()
.property_metadata()
.iter()
.find(|meta| meta.property_identifier == row)
.unwrap()
.write_capability;
assert_eq!(capability, PropertyWriteCapability::Always, "{row}");
}
write(
&mut db,
P::BACNET_IP_NAT_TRAVERSAL,
PropertyValue::Boolean(true),
)
.unwrap();
assert!(db.check_network_port_changes().is_err());
write(&mut db, P::BACNET_IP_GLOBAL_ADDRESS, written(&global())).unwrap();
assert_eq!(
read(&db, P::CHANGES_PENDING).unwrap(),
PropertyValue::Boolean(true)
);
assert_eq!(global_read(&db), global());
assert_eq!(
read(&db, P::BACNET_IP_NAT_TRAVERSAL).unwrap(),
PropertyValue::Boolean(true)
);
assert!(!*tables.nat.lock().unwrap(), "not yet in effect");
assert_eq!(*tables.global.lock().unwrap(), UNSET);
db.check_network_port_changes().unwrap();
assert_eq!(db.activate_network_port_changes().unwrap(), 1);
assert!(*tables.nat.lock().unwrap());
assert_eq!(*tables.global.lock().unwrap(), global());
let conformance = db
.get(&oid())
.unwrap()
.property_metadata()
.iter()
.find(|meta| meta.property_identifier == P::BACNET_IP_GLOBAL_ADDRESS)
.unwrap()
.conformance;
assert_eq!(
conformance,
crate::property_metadata::PropertyConformance::RequiredRead
);
let activated = tables.activated.lock().unwrap();
assert_eq!(activated.len(), 1, "both in one activation");
assert_eq!(activated[0].nat_traversal, Some(true));
drop(activated);
assert_eq!(
read(&db, P::CHANGES_PENDING).unwrap(),
PropertyValue::Boolean(false)
);
}
#[test]
fn a_global_address_the_bbmd_cant_take_is_refused_when_written() {
let tables = Tables::new(true);
let (mut db, _lease) = bbmd_port(&tables, true);
let name = BACnetHostNPort {
host: BACnetHostAddress::Name("nat.example".into()),
port: 47809,
};
assert!(write(&mut db, P::BACNET_IP_GLOBAL_ADDRESS, written(&name)).is_err());
for wrong in [
PropertyValue::Boolean(true),
PropertyValue::ApplicationData(vec![0x0E]),
] {
assert!(write(&mut db, P::BACNET_IP_GLOBAL_ADDRESS, wrong).is_err());
}
assert!(
write(
&mut db,
P::BACNET_IP_NAT_TRAVERSAL,
PropertyValue::Unsigned(1)
)
.is_err()
);
assert_eq!(
read(&db, P::CHANGES_PENDING).unwrap(),
PropertyValue::Boolean(false)
);
}
#[test]
fn without_activation_the_nat_rows_are_read_only() {
let tables = Tables::new(true);
let (mut db, _lease) = bbmd_port(&tables, false);
let error = write(&mut db, P::BACNET_IP_GLOBAL_ADDRESS, written(&global())).unwrap_err();
assert!(
matches!(error, Error::Remote(RemoteError::Error { code, .. }) if code == ErrorCode::WRITE_ACCESS_DENIED),
"{error:?}"
);
let capability = db
.get(&oid())
.unwrap()
.property_metadata()
.iter()
.find(|meta| meta.property_identifier == P::BACNET_IP_NAT_TRAVERSAL)
.unwrap()
.write_capability;
assert_eq!(capability, PropertyWriteCapability::ReadOnly);
}