diff --git a/crates/lanparty-relay/src/server.rs b/crates/lanparty-relay/src/server.rs index df4cb37..a8d4b8d 100644 --- a/crates/lanparty-relay/src/server.rs +++ b/crates/lanparty-relay/src/server.rs @@ -1026,10 +1026,17 @@ mod tests { const ARP_REQUEST: u16 = 1; const ARP_REPLY: u16 = 2; const IPV4_HEADER_LEN: usize = 20; + const UDP_HEADER_LEN: usize = 8; const IP_PROTOCOL_ICMPV4: u8 = 1; const IP_PROTOCOL_UDP: u8 = 17; const ICMPV4_ECHO_REPLY: u8 = 0; const ICMPV4_ECHO_REQUEST: u8 = 8; + const DHCPV4_SERVER_PORT: u16 = 67; + const DHCPV4_CLIENT_PORT: u16 = 68; + const DHCPV4_BOOTREQUEST: u8 = 1; + const DHCPV4_BOOTREPLY: u8 = 2; + const DHCPV4_DISCOVER: u8 = 1; + const DHCPV4_OFFER: u8 = 2; const LAN_GAME_DISCOVERY_PORT: u16 = 27015; #[tokio::test] @@ -1771,6 +1778,151 @@ mod tests { assert!(sessions.lock().await.is_empty()); } + #[tokio::test] + async fn bridges_dhcpv4_frames_between_client_and_gateway_sessions() { + let (server, certificate) = bind_test_server(DEFAULT_MAX_CLIENTS_PER_ROOM); + let rooms = Arc::clone(&server.rooms); + let sessions = Arc::clone(&server.sessions); + let server_addr = server.local_addr().unwrap(); + let server_task = tokio::spawn(async move { + let handles = server.accept_many_for_test(2).await.unwrap(); + let mut accepted = Vec::with_capacity(handles.len()); + + for handle in handles { + accepted.push(handle.await.unwrap().unwrap().unwrap()); + } + + server.shutdown("test complete").await; + accepted + }); + let cert_der = certificate.as_ref().to_vec(); + let room = RoomCode::new("TESTROOM").unwrap(); + let client_mac = client_mac(1); + let client_ip = Ipv4Addr::new(10, 73, 42, 51); + let dhcp_server_mac = lan_host_mac(); + let dhcp_server_ip = Ipv4Addr::new(10, 73, 42, 1); + let gateway = connect_gateway( + GatewayConfig::new( + server_addr, + "lanparty-relay.local", + cert_der.clone(), + room.clone(), + "eth0", + 1400, + ) + .unwrap(), + ) + .await + .unwrap(); + let client = connect_client( + ClientSessionConfig::new( + server_addr, + "lanparty-relay.local", + cert_der, + room, + client_mac, + 1400, + ) + .unwrap(), + ) + .await + .unwrap(); + + let ControlMessage::PeerJoined(peer) = + tokio::time::timeout(Duration::from_secs(5), gateway.recv_control_event()) + .await + .unwrap() + .unwrap() + else { + panic!("expected gateway to observe client join"); + }; + assert_eq!(peer.peer_id(), client.welcome().peer_id()); + assert_eq!(peer.role(), Role::Client); + assert_eq!(peer.mac(), Some(client_mac)); + + let ControlMessage::PeerJoined(peer) = + tokio::time::timeout(Duration::from_secs(5), client.recv_control_event()) + .await + .unwrap() + .unwrap() + else { + panic!("expected client to receive gateway catch-up event"); + }; + assert_eq!(peer.peer_id(), gateway.welcome().peer_id()); + assert_eq!(peer.role(), Role::Gateway); + + let discover_payload = + dhcpv4_payload(DHCPV4_BOOTREQUEST, DHCPV4_DISCOVER, client_mac, None); + let discover = udp_ipv4_frame( + MacAddr::BROADCAST, + client_mac, + Ipv4Addr::new(0, 0, 0, 0), + Ipv4Addr::new(255, 255, 255, 255), + DHCPV4_CLIENT_PORT, + DHCPV4_SERVER_PORT, + &discover_payload, + ); + let discover_header = EthernetFrame::parse(&discover).unwrap(); + assert!(discover_header.is_broadcast()); + assert_eq!(discover_header.ethertype_or_len(), ETHERTYPE_IPV4); + assert_udp_ports(&discover, DHCPV4_CLIENT_PORT, DHCPV4_SERVER_PORT); + assert_eq!(dhcpv4_message_type(&discover), Some(DHCPV4_DISCOVER)); + assert_eq!( + client + .relay_io() + .send_ethernet_with_outcome(&discover) + .unwrap(), + lanparty_client_core::ClientSendOutcome::Sent + ); + let received = tokio::time::timeout(Duration::from_secs(5), gateway.recv_ethernet()) + .await + .unwrap() + .unwrap(); + assert_eq!(received.source_peer_id(), client.welcome().peer_id()); + assert_eq!(received.payload(), discover.as_slice()); + + let offer_payload = + dhcpv4_payload(DHCPV4_BOOTREPLY, DHCPV4_OFFER, client_mac, Some(client_ip)); + let offer = udp_ipv4_frame( + MacAddr::BROADCAST, + dhcp_server_mac, + dhcp_server_ip, + Ipv4Addr::new(255, 255, 255, 255), + DHCPV4_SERVER_PORT, + DHCPV4_CLIENT_PORT, + &offer_payload, + ); + let offer_header = EthernetFrame::parse(&offer).unwrap(); + assert!(offer_header.is_broadcast()); + assert_eq!(offer_header.ethertype_or_len(), ETHERTYPE_IPV4); + assert_udp_ports(&offer, DHCPV4_SERVER_PORT, DHCPV4_CLIENT_PORT); + assert_eq!(dhcpv4_message_type(&offer), Some(DHCPV4_OFFER)); + gateway.send_ethernet(&offer).unwrap(); + let received = + tokio::time::timeout(Duration::from_secs(5), client.relay_io().recv_ethernet()) + .await + .unwrap() + .unwrap(); + assert_eq!(received.source_peer_id(), gateway.welcome().peer_id()); + assert_eq!(received.payload(), offer.as_slice()); + + assert_eq!(client.stats_snapshot().broadcast_frames_tx(), 1); + assert_eq!(client.stats_snapshot().broadcast_frames_rx(), 1); + assert_eq!(gateway.stats_snapshot().broadcast_frames_tx(), 1); + assert_eq!(gateway.stats_snapshot().broadcast_frames_rx(), 1); + + client.shutdown("test client done").await; + gateway.shutdown("test gateway done").await; + + let accepted = tokio::time::timeout(Duration::from_secs(5), server_task) + .await + .unwrap() + .unwrap(); + assert_eq!(accepted.len(), 2); + assert_eq!(rooms.lock().await.room_count(), 0); + assert!(sessions.lock().await.is_empty()); + } + #[tokio::test] async fn bridges_icmpv4_ping_frames_between_client_and_gateway_sessions() { let (server, certificate) = bind_test_server(DEFAULT_MAX_CLIENTS_PER_ROOM); @@ -2513,6 +2665,43 @@ mod tests { ethernet_frame_with_payload(destination, source, ETHERTYPE_IPV4, &ipv4) } + fn dhcpv4_payload( + operation: u8, + message_type: u8, + client_mac: MacAddr, + offered_ip: Option, + ) -> Vec { + let mut payload = vec![0_u8; 240]; + payload[0] = operation; + payload[1] = 1; + payload[2] = 6; + payload[4..8].copy_from_slice(&0x1234_5678_u32.to_be_bytes()); + payload[10..12].copy_from_slice(&0x8000_u16.to_be_bytes()); + if let Some(offered_ip) = offered_ip { + payload[16..20].copy_from_slice(&offered_ip.octets()); + } + payload[28..34].copy_from_slice(&client_mac.octets()); + payload[236..240].copy_from_slice(&[99, 130, 83, 99]); + payload.extend_from_slice(&[53, 1, message_type, 255]); + + payload + } + + fn assert_udp_ports(frame: &[u8], source_port: u16, destination_port: u16) { + let ipv4 = &frame[ETHERNET_HEADER_LEN..]; + assert_eq!(ipv4[9], IP_PROTOCOL_UDP); + let udp = &ipv4[IPV4_HEADER_LEN..]; + assert_eq!(u16::from_be_bytes([udp[0], udp[1]]), source_port); + assert_eq!(u16::from_be_bytes([udp[2], udp[3]]), destination_port); + } + + fn dhcpv4_message_type(frame: &[u8]) -> Option { + let payload = &frame[ETHERNET_HEADER_LEN + IPV4_HEADER_LEN + UDP_HEADER_LEN..]; + payload + .windows(3) + .find_map(|option| (option[0] == 53 && option[1] == 1).then_some(option[2])) + } + fn ipv4_packet( protocol: u8, source_ip: Ipv4Addr,