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:
Marcel Müller 2026-02-09 08:42:27 +01:00 committed by GitHub
commit 6193fe4cf1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 78 additions and 33 deletions

View file

@ -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),
);

View file

@ -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,6 +546,8 @@
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);
@ -550,7 +561,7 @@
// 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,
@ -683,11 +695,14 @@
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,7 +799,8 @@
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`);
@ -799,7 +815,7 @@
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);
@ -813,7 +829,9 @@
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}`));
@ -828,7 +846,8 @@
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`);
@ -853,7 +872,9 @@
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}`));
@ -869,7 +890,9 @@
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))