Merge pull request #1 from TheNeikos/claude/fix-ice-direct-ip-Q0OBq
Add IP address tracking and mDNS resolution for direct LAN connectivity
This commit is contained in:
commit
6193fe4cf1
2 changed files with 78 additions and 33 deletions
|
|
@ -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<PeerInfo>,
|
||||
},
|
||||
/// 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<SignalMessage>,
|
||||
|
|
@ -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::<SocketAddr>())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn serve_index() -> impl IntoResponse {
|
||||
Html(include_str!("../static/index.html"))
|
||||
}
|
||||
|
||||
async fn ws_handler(ws: WebSocketUpgrade, State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
ws.on_upgrade(move |socket| handle_socket(socket, state))
|
||||
async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(move |socket| handle_socket(socket, state, addr))
|
||||
}
|
||||
|
||||
async fn handle_socket(socket: WebSocket, state: Arc<AppState>) {
|
||||
async fn handle_socket(socket: WebSocket, state: Arc<AppState>, addr: SocketAddr) {
|
||||
let peer_id = Uuid::new_v4().to_string();
|
||||
let peer_ip = addr.ip().to_string();
|
||||
let (tx, _) = broadcast::channel::<SignalMessage>(64);
|
||||
|
||||
// Add peer to state with default name
|
||||
|
|
@ -236,6 +250,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>) {
|
|||
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<AppState>) {
|
|||
// 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<AppState>) {
|
|||
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),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue