diff --git a/lanshare/src/main.rs b/lanshare/src/main.rs index 270521b..04fd728 100644 --- a/lanshare/src/main.rs +++ b/lanshare/src/main.rs @@ -1,5 +1,6 @@ use axum::{ extract::{ + connect_info::ConnectInfo, ws::{Message, WebSocket, WebSocketUpgrade}, State, }, @@ -63,6 +64,7 @@ enum SignalMessage { /// Server assigns an ID to a new peer Welcome { peer_id: String, + ip: String, peers: Vec, }, /// Peer announces themselves (with optional name) @@ -73,6 +75,7 @@ enum SignalMessage { PeerJoined { peer_id: String, name: String, + ip: String, }, /// Broadcast: a peer left PeerLeft { @@ -120,6 +123,7 @@ enum SignalMessage { struct PeerInfo { peer_id: String, name: String, + ip: String, is_sharing: bool, has_audio: bool, } @@ -127,6 +131,7 @@ struct PeerInfo { /// State for a connected peer struct PeerState { name: String, + ip: String, is_sharing: bool, has_audio: bool, tx: broadcast::Sender, @@ -150,6 +155,7 @@ impl AppState { .map(|entry| PeerInfo { peer_id: entry.key().clone(), name: entry.value().name.clone(), + ip: entry.value().ip.clone(), is_sharing: entry.value().is_sharing, has_audio: entry.value().has_audio, }) @@ -216,19 +222,27 @@ async fn main() { let config = RustlsConfig::from_pem_file(args.certificate, args.key).await.unwrap(); - axum_server::bind_rustls(addr, config).serve(app.into_make_service()).await.unwrap(); + axum_server::bind_rustls(addr, config) + .serve(app.into_make_service_with_connect_info::()) + .await + .unwrap(); } async fn serve_index() -> impl IntoResponse { Html(include_str!("../static/index.html")) } -async fn ws_handler(ws: WebSocketUpgrade, State(state): State>) -> impl IntoResponse { - ws.on_upgrade(move |socket| handle_socket(socket, state)) +async fn ws_handler( + ws: WebSocketUpgrade, + ConnectInfo(addr): ConnectInfo, + State(state): State>, +) -> impl IntoResponse { + ws.on_upgrade(move |socket| handle_socket(socket, state, addr)) } -async fn handle_socket(socket: WebSocket, state: Arc) { +async fn handle_socket(socket: WebSocket, state: Arc, addr: SocketAddr) { let peer_id = Uuid::new_v4().to_string(); + let peer_ip = addr.ip().to_string(); let (tx, _) = broadcast::channel::(64); // Add peer to state with default name @@ -236,6 +250,7 @@ async fn handle_socket(socket: WebSocket, state: Arc) { peer_id.clone(), PeerState { name: format!("Peer {}", &peer_id[..8]), + ip: peer_ip.clone(), is_sharing: false, has_audio: false, tx: tx.clone(), @@ -250,6 +265,7 @@ async fn handle_socket(socket: WebSocket, state: Arc) { // Send welcome message with current peer list let welcome = SignalMessage::Welcome { peer_id: peer_id.clone(), + ip: peer_ip, peers: peers.into_iter().filter(|p| p.peer_id != peer_id).collect(), }; @@ -319,6 +335,11 @@ async fn handle_socket(socket: WebSocket, state: Arc) { async fn handle_signal(state: &AppState, from_peer: &str, signal: SignalMessage) { match signal { SignalMessage::Announce { name } => { + let ip = state + .peers + .get(from_peer) + .map(|p| p.ip.clone()) + .unwrap_or_default(); if let Some(mut peer) = state.peers.get_mut(from_peer) { peer.name = name.clone(); } @@ -326,6 +347,7 @@ async fn handle_signal(state: &AppState, from_peer: &str, signal: SignalMessage) SignalMessage::PeerJoined { peer_id: from_peer.to_string(), name, + ip, }, Some(from_peer), ); diff --git a/lanshare/static/index.html b/lanshare/static/index.html index 20efc15..60cf32a 100644 --- a/lanshare/static/index.html +++ b/lanshare/static/index.html @@ -428,8 +428,9 @@ // ==================== State ==================== const state = { peerId: null, + peerIp: null, // Our own IP as seen by the server peerName: localStorage.getItem('lanshare_name') || null, - peers: new Map(), // peer_id -> { name, isSharing, hasAudio, stream, peerConnection } + peers: new Map(), // peer_id -> { name, ip, isSharing, hasAudio, stream, peerConnection } localStream: null, isSharing: false, includeAudio: true, @@ -456,6 +457,14 @@ return candidates; } + // Replace mDNS .local addresses with a real IP. + // Modern browsers obfuscate local IPs as UUIDs ending in .local for privacy, + // which breaks direct LAN connections when mDNS resolution isn't available. + function replaceMdnsWithIp(str, realIp) { + if (!str || !realIp) return str; + return str.replace(/[a-f0-9-]+\.local/gi, realIp); + } + console.log(`ICE mode: ${USE_TRICKLE_ICE ? 'TRICKLE' : 'BUNDLED'}`); // ==================== DOM Elements ==================== @@ -537,20 +546,22 @@ switch (msg.type) { case 'welcome': state.peerId = msg.peer_id; + state.peerIp = msg.ip; + console.log(`Our IP as seen by server: ${msg.ip}`); connectingOverlay.style.display = 'none'; updateConnectionStatus(true); - + // Set name if (!state.peerName) { state.peerName = prompt('Enter your name:', `User ${msg.peer_id.slice(0, 4)}`) || `User ${msg.peer_id.slice(0, 4)}`; localStorage.setItem('lanshare_name', state.peerName); } - + send({ type: 'announce', name: state.peerName }); - + // Add existing peers for (const peer of msg.peers) { - addPeer(peer.peer_id, peer.name, peer.is_sharing, peer.has_audio); + addPeer(peer.peer_id, peer.name, peer.ip, peer.is_sharing, peer.has_audio); // Request stream from peers that are already sharing if (peer.is_sharing) { requestStreamFrom(peer.peer_id); @@ -560,7 +571,7 @@ break; case 'peer_joined': - addPeer(msg.peer_id, msg.name, false, false); + addPeer(msg.peer_id, msg.name, msg.ip, false, false); // If we're sharing, proactively send an offer to the new peer if (state.isSharing && state.localStream) { await sendOfferTo(msg.peer_id); @@ -607,7 +618,7 @@ if (state.isSharing && state.localStream) { // Ensure we have this peer in our state (handles race condition) if (!state.peers.has(msg.from)) { - addPeer(msg.from, `Peer ${msg.from.slice(0, 4)}`, false, false); + addPeer(msg.from, `Peer ${msg.from.slice(0, 4)}`, null, false, false); } await sendOfferTo(msg.from); } @@ -628,11 +639,12 @@ } // ==================== Peer Management ==================== - function addPeer(peerId, name, isSharing, hasAudio) { + function addPeer(peerId, name, ip, isSharing, hasAudio) { if (peerId === state.peerId) return; - + state.peers.set(peerId, { name, + ip, isSharing, hasAudio, stream: null, @@ -681,13 +693,16 @@ const c = event.candidate; console.log(`[${peerId.slice(0,8)}] Local candidate: ${c.candidate}`); console.log(` → type: ${c.type}, protocol: ${c.protocol}, address: ${c.address}, port: ${c.port}`); - + if (USE_TRICKLE_ICE) { + // Replace mDNS .local addresses with our real IP for direct LAN connectivity + const candidateObj = event.candidate.toJSON(); + candidateObj.candidate = replaceMdnsWithIp(candidateObj.candidate, state.peerIp); send({ type: 'ice_candidate', from: state.peerId, to: peerId, - candidate: JSON.stringify(event.candidate), + candidate: JSON.stringify(candidateObj), }); } } else { @@ -784,10 +799,11 @@ await waitForIceGathering(pc, peerId); } - const sdpToSend = pc.localDescription; + // Replace mDNS .local addresses with our real IP for direct LAN connectivity + const sdpToSend = { type: pc.localDescription.type, sdp: replaceMdnsWithIp(pc.localDescription.sdp, state.peerIp) }; const candidates = extractCandidatesFromSDP(sdpToSend.sdp); console.log(`[${peerId.slice(0,8)}] Sending offer with ${candidates.length} candidates in SDP`); - + send({ type: 'offer', from: state.peerId, @@ -799,39 +815,42 @@ async function handleOffer(fromPeerId, sdpJson) { // Ensure we have this peer in state if (!state.peers.has(fromPeerId)) { - addPeer(fromPeerId, `Peer ${fromPeerId.slice(0, 4)}`, true, false); + addPeer(fromPeerId, `Peer ${fromPeerId.slice(0, 4)}`, null, true, false); } - + const peer = state.peers.get(fromPeerId); - + // Close existing connection if any (new offer = renegotiation) if (peer.peerConnection) { peer.peerConnection.close(); } - + console.log(`[${fromPeerId.slice(0,8)}] handleOffer: creating answer (${USE_TRICKLE_ICE ? 'trickle' : 'bundled'})`); const pc = createPeerConnection(fromPeerId); peer.peerConnection = pc; - + + // Replace any mDNS .local addresses in the remote SDP with the peer's real IP const sdp = JSON.parse(sdpJson); + sdp.sdp = replaceMdnsWithIp(sdp.sdp, peer.ip); const remoteCandidates = extractCandidatesFromSDP(sdp.sdp); console.log(`[${fromPeerId.slice(0,8)}] Received offer with ${remoteCandidates.length} candidates in SDP:`); remoteCandidates.forEach(c => console.log(` ${c}`)); - + await pc.setRemoteDescription(new RTCSessionDescription(sdp)); - + const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); - + if (!USE_TRICKLE_ICE) { // Bundled mode: wait for all candidates to be in SDP await waitForIceGathering(pc, fromPeerId); } - - const sdpToSend = pc.localDescription; + + // Replace mDNS .local addresses with our real IP for direct LAN connectivity + const sdpToSend = { type: pc.localDescription.type, sdp: replaceMdnsWithIp(pc.localDescription.sdp, state.peerIp) }; const localCandidates = extractCandidatesFromSDP(sdpToSend.sdp); console.log(`[${fromPeerId.slice(0,8)}] Sending answer with ${localCandidates.length} candidates in SDP`); - + send({ type: 'answer', from: state.peerId, @@ -846,18 +865,20 @@ console.warn(`[${fromPeerId.slice(0,8)}] handleAnswer: no peer connection`); return; } - + // Only set remote description if we're in the right state if (peer.peerConnection.signalingState !== 'have-local-offer') { console.warn(`[${fromPeerId.slice(0,8)}] handleAnswer: wrong state: ${peer.peerConnection.signalingState}`); return; } - + + // Replace any mDNS .local addresses in the remote SDP with the peer's real IP const sdp = JSON.parse(sdpJson); + sdp.sdp = replaceMdnsWithIp(sdp.sdp, peer.ip); const remoteCandidates = extractCandidatesFromSDP(sdp.sdp); console.log(`[${fromPeerId.slice(0,8)}] Received answer with ${remoteCandidates.length} candidates in SDP:`); remoteCandidates.forEach(c => console.log(` ${c}`)); - + console.log(`[${fromPeerId.slice(0,8)}] handleAnswer: setting remote description`); await peer.peerConnection.setRemoteDescription(new RTCSessionDescription(sdp)); } @@ -868,10 +889,12 @@ console.warn(`[${fromPeerId.slice(0,8)}] handleIceCandidate: no peer connection`); return; } - + + // Replace any mDNS .local addresses with the peer's real IP const candidate = JSON.parse(candidateJson); + candidate.candidate = replaceMdnsWithIp(candidate.candidate, peer.ip); console.log(`[${fromPeerId.slice(0,8)}] Remote candidate: ${candidate.candidate}`); - + peer.peerConnection.addIceCandidate(new RTCIceCandidate(candidate)) .then(() => console.log(`[${fromPeerId.slice(0,8)}] Successfully added remote candidate`)) .catch(err => console.error(`[${fromPeerId.slice(0,8)}] Error adding ICE candidate:`, err));