Fix ICE failures by replacing mDNS .local addresses with real peer IPs

Modern browsers obfuscate local IP addresses with mDNS hostnames
(e.g. abcdef12-3456.local) for privacy. When mDNS resolution is
unavailable on the LAN, WebRTC ICE candidates cannot be resolved
and the peer connection fails.

This fix makes the signaling server extract each client's real IP
from the TCP connection and share it via Welcome/PeerJoined messages.
The client then replaces any .local mDNS addresses in SDP offers,
answers, and trickle ICE candidates with the peer's actual IP address,
enabling direct IP connectivity without relying on mDNS.

https://claude.ai/code/session_01EZrFdyAR35RLwRdHUA9zPJ
This commit is contained in:
Claude 2026-02-09 07:38:49 +00:00
parent accbfdb1ea
commit cc57356a53
No known key found for this signature in database
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),
);