fix(gateway): refresh CAM on client joins
The gateway already emits periodic CAM refresh frames so the physical switch keeps remote client MACs on the gateway port. A newly joined client previously waited until either its own tunneled traffic reached the LAN or the first 60-second refresh tick fired. Emit one padded CAM refresh frame immediately when a valid client PeerJoined control event is observed. This makes the switch MAC-table check in the MVP procedure visible sooner and keeps the periodic refresh as the aging guard. The refresh uses the same maintenance frame shape as the periodic path and is logged with the client peer id and MAC. README.md and TESTING.md now document the immediate refresh behavior and the log signal to look for during manual LAN testing. Test Plan: - cargo test -p lanparty-gateway - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - cargo fmt --check - git diff --check Refs: PLAN.md CAM refresh; TESTING.md MVP switch MAC-table check
This commit is contained in:
@@ -435,8 +435,20 @@ impl GatewayConnection {
|
||||
let control_stream = control_stream
|
||||
.context("failed to accept gateway control event stream")?;
|
||||
let control_event = read_gateway_control_event(control_stream).await?;
|
||||
remote_clients.observe_control_event(&control_event);
|
||||
let immediate_refresh = remote_clients.observe_control_event(&control_event);
|
||||
println!("{}", format_gateway_control_event(&control_event));
|
||||
if let Some(refresh) = immediate_refresh {
|
||||
write_lan_ethernet(&packet_socket, refresh.frame()).await?;
|
||||
println!(
|
||||
"{}",
|
||||
gateway_cam_refresh_log_line(
|
||||
packet_socket.get_ref().interface(),
|
||||
refresh.peer_id(),
|
||||
refresh.mac(),
|
||||
"peer_joined",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -760,6 +772,16 @@ fn gateway_frame_log_line(
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn gateway_cam_refresh_log_line(
|
||||
interface: &str,
|
||||
peer_id: u32,
|
||||
mac: MacAddr,
|
||||
reason: &str,
|
||||
) -> String {
|
||||
format!("gateway CAM refresh interface={interface} peer_id={peer_id} mac={mac} reason={reason}")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn read_lan_ethernet(packet_socket: &AsyncFd<PacketSocket>) -> Result<Bytes> {
|
||||
loop {
|
||||
@@ -803,6 +825,37 @@ struct RemoteClientTable {
|
||||
remote_clients: BTreeMap<u32, MacAddr>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct CamRefresh {
|
||||
peer_id: u32,
|
||||
mac: MacAddr,
|
||||
frame: Vec<u8>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl CamRefresh {
|
||||
fn new(peer_id: u32, mac: MacAddr, gateway_mac: MacAddr) -> Self {
|
||||
Self {
|
||||
peer_id,
|
||||
mac,
|
||||
frame: cam_refresh_frame(mac, gateway_mac),
|
||||
}
|
||||
}
|
||||
|
||||
const fn peer_id(&self) -> u32 {
|
||||
self.peer_id
|
||||
}
|
||||
|
||||
const fn mac(&self) -> MacAddr {
|
||||
self.mac
|
||||
}
|
||||
|
||||
fn frame(&self) -> &[u8] {
|
||||
&self.frame
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl RemoteClientTable {
|
||||
fn new(gateway_mac: MacAddr) -> Self {
|
||||
@@ -824,21 +877,27 @@ impl RemoteClientTable {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn observe_control_event(&mut self, event: &ControlMessage) {
|
||||
fn observe_control_event(&mut self, event: &ControlMessage) -> Option<CamRefresh> {
|
||||
match event {
|
||||
ControlMessage::PeerJoined(peer) => self.observe_peer_joined(peer),
|
||||
ControlMessage::PeerLeft { peer_id, .. } => self.observe_peer_left(*peer_id),
|
||||
_ => {}
|
||||
ControlMessage::PeerLeft { peer_id, .. } => {
|
||||
self.observe_peer_left(*peer_id);
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn observe_peer_joined(&mut self, peer: &PeerInfo) {
|
||||
fn observe_peer_joined(&mut self, peer: &PeerInfo) -> Option<CamRefresh> {
|
||||
if peer.role() == Role::Client
|
||||
&& let Some(mac) = peer.mac()
|
||||
&& mac.is_valid_client_identity()
|
||||
{
|
||||
self.remote_clients.insert(peer.peer_id(), mac);
|
||||
return Some(CamRefresh::new(peer.peer_id(), mac, self.gateway_mac));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn observe_peer_left(&mut self, peer_id: u32) {
|
||||
@@ -1243,9 +1302,10 @@ mod tests {
|
||||
Some(DropReason::UnauthorizedSourceMac)
|
||||
);
|
||||
|
||||
remote_clients.observe_control_event(&ControlMessage::PeerJoined(
|
||||
let refresh = remote_clients.observe_control_event(&ControlMessage::PeerJoined(
|
||||
PeerInfo::new(7, Role::Client, Some(remote_mac)).unwrap(),
|
||||
));
|
||||
assert!(refresh.is_some());
|
||||
|
||||
assert_eq!(
|
||||
remote_clients
|
||||
@@ -1274,9 +1334,16 @@ mod tests {
|
||||
let remote_mac = MacAddr::new([0x02, 0, 0, 0, 0, 2]);
|
||||
let mut refresh = RemoteClientTable::new(gateway_mac);
|
||||
|
||||
refresh.observe_control_event(&ControlMessage::PeerJoined(
|
||||
let immediate_refresh = refresh.observe_control_event(&ControlMessage::PeerJoined(
|
||||
PeerInfo::new(7, Role::Client, Some(remote_mac)).unwrap(),
|
||||
));
|
||||
let immediate_refresh = immediate_refresh.expect("client joins trigger CAM refresh");
|
||||
let immediate_frame = EthernetFrame::parse(immediate_refresh.frame()).unwrap();
|
||||
|
||||
assert_eq!(immediate_refresh.peer_id(), 7);
|
||||
assert_eq!(immediate_refresh.mac(), remote_mac);
|
||||
assert_eq!(immediate_frame.source(), remote_mac);
|
||||
assert_eq!(immediate_frame.destination(), gateway_mac);
|
||||
assert_eq!(refresh.remote_mac_count(), 1);
|
||||
assert_eq!(
|
||||
EthernetFrame::parse(&refresh.refresh_frames()[0])
|
||||
@@ -1285,10 +1352,13 @@ mod tests {
|
||||
remote_mac
|
||||
);
|
||||
|
||||
refresh.observe_control_event(&ControlMessage::PeerLeft {
|
||||
peer_id: 7,
|
||||
reason: DisconnectReason::Normal,
|
||||
});
|
||||
assert_eq!(
|
||||
refresh.observe_control_event(&ControlMessage::PeerLeft {
|
||||
peer_id: 7,
|
||||
reason: DisconnectReason::Normal,
|
||||
}),
|
||||
None
|
||||
);
|
||||
assert_eq!(refresh.remote_mac_count(), 0);
|
||||
assert!(refresh.refresh_frames().is_empty());
|
||||
}
|
||||
@@ -1311,6 +1381,20 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn formats_gateway_cam_refresh_log_line() {
|
||||
assert_eq!(
|
||||
gateway_cam_refresh_log_line(
|
||||
"eth0",
|
||||
7,
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, 2]),
|
||||
"peer_joined"
|
||||
),
|
||||
"gateway CAM refresh interface=eth0 peer_id=7 mac=02:00:00:00:00:02 reason=peer_joined"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn formats_gateway_datagram_budget_drops() {
|
||||
|
||||
Reference in New Issue
Block a user