1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
use super::*;
use bacnet_endpoint_core::coordinator::response_invoke_id;
/// The peer a confirmed transaction is keyed to: its canonical identity and
/// the TSM key derived from it. The standalone client and the endpoint
/// requester both key requests and match answers through here, so the two
/// sides of a transaction always agree.
pub(crate) struct TransactionPeer {
pub(crate) tsm_mac: MacAddr,
pub(crate) canonical: CanonicalPeer,
}
impl TransactionPeer {
/// The transaction peer for `canonical`. A direct peer's TSM key is its
/// MAC. A routed peer's is the `FF 52` synthetic key (`network || len ||
/// address`), so it never collides with a direct peer that happens to
/// share trailing bytes.
pub(crate) fn of(canonical: CanonicalPeer) -> Self {
let tsm_mac = match &canonical {
CanonicalPeer::Direct(mac) => mac.clone(),
CanonicalPeer::Routed { network, address } => routed_tsm_mac(*network, address),
};
Self { tsm_mac, canonical }
}
/// The transaction an answer belongs to, from the answer's link MAC and
/// SNET/SADR. With `local_network` known, an answer a router relays back
/// with this network as its SNET belongs to the same transaction as one
/// sent straight from its SADR (#1465); see [`CanonicalPeer::from_source`].
pub(crate) fn of_answer(
source_mac: &[u8],
source_network: Option<&NpduAddress>,
local_network: Option<u16>,
) -> Self {
Self::of(CanonicalPeer::from_source(
source_mac,
source_network,
local_network,
))
}
/// [`Self::of_answer`] for an answer to `apdu`, checked against the
/// pending transactions in `tsm`. A request routed to this network while
/// its number was unknown stays keyed to the routed form once the number
/// is learned. When no transaction with the answer's invoke ID is keyed
/// to the direct station and one is keyed to that routed form
/// ([`CanonicalPeer::routed_alias`]), the answer belongs to it, as it did
/// before the number was known (#1465). The TSM is locked only for an
/// answer relayed with this network's number as SNET.
pub(crate) async fn of_answer_in(
tsm: &Mutex<Tsm>,
source_mac: &[u8],
source_network: Option<&NpduAddress>,
local_network: Option<u16>,
apdu: &Apdu,
) -> Self {
let answer = Self::of_answer(source_mac, source_network, local_network);
let alias = CanonicalPeer::routed_alias(source_network, local_network);
let (Some(invoke_id), Some(alias)) = (response_invoke_id(apdu), alias) else {
return answer;
};
let alias = Self::of(alias);
let tsm = tsm.lock().await;
if !tsm.has_transaction(&answer.tsm_mac, invoke_id)
&& tsm.has_transaction(&alias.tsm_mac, invoke_id)
{
alias
} else {
answer
}
}
}
impl ConfirmedTarget<'_> {
/// This target as it is sent once the client knows `local_network`, the
/// number of its own network (#1358). A routed target on that network is
/// a local one: the DADR is a MAC on this link, so the request goes there
/// with no DNET and not through the router (Clause 6.5.1), since a
/// non-routing peer drops an NPDU whose DNET names a network (Clause
/// 6.5.2.1). Its answer then comes from that MAC with no SNET, or through
/// a router with this network as its SNET, and either completes the
/// transaction keyed to the local peer (#1465). Any other target, and
/// every target while the number is unknown, is unchanged.
pub(super) fn localized(self, local_network: Option<u16>) -> Self {
match self {
Self::Routed {
dest_network,
dest_mac,
..
} if Some(dest_network) == local_network => Self::Local { mac: dest_mac },
target => target,
}
}
pub(super) fn transaction_peer(self) -> TransactionPeer {
TransactionPeer::of(match self {
Self::Local { mac } => CanonicalPeer::direct(mac),
Self::Routed {
dest_network,
dest_mac,
..
} => CanonicalPeer::routed(dest_network, dest_mac),
})
}
}
fn routed_tsm_mac(network: u16, mac: &[u8]) -> MacAddr {
let mut key = MacAddr::new();
key.extend_from_slice(&[0xFF, b'R']);
key.extend_from_slice(&network.to_be_bytes());
key.push(mac.len() as u8);
key.extend_from_slice(mac);
key
}
#[cfg(test)]
mod tests {
use super::*;
fn address(network: u16, mac: &[u8]) -> NpduAddress {
NpduAddress {
network,
mac_address: MacAddr::from_slice(mac),
}
}
#[test]
fn outbound_and_inbound_transaction_identities_agree() {
let direct = ConfirmedTarget::Local { mac: &[1, 2, 3] }.transaction_peer();
let direct_response = TransactionPeer::of_answer(&[1, 2, 3], None, None);
assert_eq!(direct.tsm_mac, direct_response.tsm_mac);
assert_eq!(direct.canonical, direct_response.canonical);
let routed = ConfirmedTarget::Routed {
router_mac: &[9],
dest_network: 42,
dest_mac: &[4, 5],
}
.transaction_peer();
let routed_response = TransactionPeer::of_answer(&[8], Some(&address(42, &[4, 5])), None);
assert_eq!(routed.tsm_mac, routed_response.tsm_mac);
assert_eq!(routed.canonical, routed_response.canonical);
}
/// A request routed to the known local network goes to the DADR and is
/// keyed to it, and an answer relayed back with that network as its SNET
/// carries the same key as one sent straight from the DADR (#1465).
#[test]
fn a_relayed_answer_from_this_network_matches_the_localized_request() {
let request = ConfirmedTarget::Routed {
router_mac: &[9],
dest_network: 42,
dest_mac: &[4, 5],
}
.localized(Some(42))
.transaction_peer();
for answer in [
TransactionPeer::of_answer(&[9], Some(&address(42, &[4, 5])), Some(42)),
TransactionPeer::of_answer(&[4, 5], None, Some(42)),
] {
assert_eq!(answer.tsm_mac, request.tsm_mac);
assert_eq!(answer.canonical, request.canonical);
}
// Another SNET, another SADR, or an unknown number keys elsewhere.
for other in [
TransactionPeer::of_answer(&[9], Some(&address(43, &[4, 5])), Some(42)),
TransactionPeer::of_answer(&[9], Some(&address(42, &[4, 6])), Some(42)),
TransactionPeer::of_answer(&[9], Some(&address(42, &[4, 5])), None),
] {
assert_ne!(other.tsm_mac, request.tsm_mac);
assert_ne!(other.canonical, request.canonical);
}
}
#[test]
fn only_a_routed_target_on_the_known_local_network_is_localized() {
let routed = |dest_network| ConfirmedTarget::Routed {
router_mac: &[9],
dest_network,
dest_mac: &[4, 5],
};
assert!(matches!(
routed(42).localized(Some(42)),
ConfirmedTarget::Local { mac: &[4, 5] }
));
for local_network in [Some(7), None] {
assert!(matches!(
routed(42).localized(local_network),
ConfirmedTarget::Routed {
router_mac: &[9],
dest_network: 42,
dest_mac: &[4, 5],
}
));
}
assert!(matches!(
ConfirmedTarget::Local { mac: &[1] }.localized(Some(42)),
ConfirmedTarget::Local { mac: &[1] }
));
}
#[test]
fn empty_routed_source_falls_back_to_immediate_mac() {
let identity = TransactionPeer::of_answer(&[7, 8], Some(&address(99, &[])), None);
assert_eq!(identity.tsm_mac, MacAddr::from_slice(&[7, 8]));
assert_eq!(identity.canonical, CanonicalPeer::direct(&[7, 8]));
}
}