@desklink/host 0.5.7 → 0.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/protocol.d.ts +1 -0
- package/dist/protocol.d.ts.map +1 -1
- package/docs/PROTOCOL.md +49 -5
- package/engine/Cargo.lock +2 -1
- package/engine/Cargo.toml +5 -2
- package/engine/native/agent_overlay_wayland.c +46 -18
- package/engine/src/hyprland.rs +38 -3
- package/engine/src/indicator.rs +34 -10
- package/engine/src/main.rs +115 -15
- package/engine/src/peer.rs +290 -18
- package/engine/src/session.rs +257 -23
- package/engine/vendor/rtc-ice/DESKLINK-PATCH.md +2 -2
- package/engine/vendor/rtc-ice/src/agent/agent_selector.rs +1 -1
- package/engine/vendor/rtc-ice/src/agent/mod.rs +1 -1
- package/engine/vendor/rtc-ice/src/dl_trace.rs +1 -1
- package/engine/vendor/rtc-ice/src/lib.rs +1 -1
- package/package.json +3 -3
package/engine/src/peer.rs
CHANGED
|
@@ -23,7 +23,7 @@ use rtc::peer_connection::configuration::media_engine::{
|
|
|
23
23
|
use rtc::peer_connection::configuration::RTCConfigurationBuilder;
|
|
24
24
|
use rtc::peer_connection::event::RTCPeerConnectionIceEvent;
|
|
25
25
|
use rtc::peer_connection::sdp::RTCSessionDescription;
|
|
26
|
-
use rtc::peer_connection::transport::{RTCIceCandidateInit, RTCIceServer};
|
|
26
|
+
use rtc::peer_connection::transport::{RTCIceCandidateInit, RTCIceProtocol, RTCIceServer};
|
|
27
27
|
use rtc::rtcp::payload_feedbacks::full_intra_request::FullIntraRequest;
|
|
28
28
|
use rtc::rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication;
|
|
29
29
|
use rtc::rtcp::receiver_report::ReceiverReport;
|
|
@@ -47,7 +47,7 @@ use webrtc::peer_connection::{
|
|
|
47
47
|
};
|
|
48
48
|
use webrtc::runtime::default_runtime;
|
|
49
49
|
|
|
50
|
-
// desklink scout trace
|
|
50
|
+
// desklink scout trace: engine-side restart timeline.
|
|
51
51
|
// Enabled with `DESKLINK_ICE_TRACE=1`; stamps microseconds since the Unix epoch
|
|
52
52
|
// so these lines interleave with the vendored ICE agent's `[ice ...]` lines in
|
|
53
53
|
// one order. Droppable: grep `desklink scout trace`.
|
|
@@ -258,7 +258,10 @@ pub enum PeerEvent {
|
|
|
258
258
|
/// Transport settings the consumer chose for this session.
|
|
259
259
|
#[derive(Debug, Clone)]
|
|
260
260
|
pub struct TransportOptions {
|
|
261
|
-
|
|
261
|
+
/// One entry per ICE server the consumer asked for, carrying every URL
|
|
262
|
+
/// that entry listed; the engine has already left out the ones the
|
|
263
|
+
/// WebRTC stack cannot dial and reported those to the consumer.
|
|
264
|
+
pub ice_servers: Vec<(Vec<String>, Option<String>, Option<String>)>,
|
|
262
265
|
/// Listen for ICE over TCP on the loopback too. A phone that reaches this
|
|
263
266
|
/// computer only through SSH has no UDP path; it forwards this port over
|
|
264
267
|
/// the SSH connection it already has, and the picture rides that.
|
|
@@ -267,6 +270,33 @@ pub struct TransportOptions {
|
|
|
267
270
|
pub pace_bps: f64,
|
|
268
271
|
}
|
|
269
272
|
|
|
273
|
+
/// The UDP sockets ICE gathers host candidates from: an ephemeral port on every
|
|
274
|
+
/// IPv4 interface and, when this computer has an IPv6 address a peer could
|
|
275
|
+
/// reach, on every IPv6 one too. Mobile carriers commonly hand a phone a global
|
|
276
|
+
/// IPv6 address, and a stateful firewall passes the simultaneous checks, so
|
|
277
|
+
/// IPv6 can be the only direct path a phone off home Wi-Fi has. The library
|
|
278
|
+
/// expands a wildcard into the interfaces' own addresses, skipping loopback and
|
|
279
|
+
/// link-local; with none left it binds the wildcard itself and offers `::`,
|
|
280
|
+
/// which no peer can use, so `[::]` is asked for only when there is one.
|
|
281
|
+
fn udp_addrs() -> Vec<std::net::SocketAddr> {
|
|
282
|
+
let has_ipv6 = rtc::shared::ifaces::ifaces()
|
|
283
|
+
.unwrap_or_default()
|
|
284
|
+
.iter()
|
|
285
|
+
.filter_map(|interface| interface.addr)
|
|
286
|
+
.any(|addr| match addr.ip() {
|
|
287
|
+
std::net::IpAddr::V6(ip) => {
|
|
288
|
+
// fe80::/10; `Ipv6Addr::is_unicast_link_local` is still unstable.
|
|
289
|
+
!ip.is_loopback() && !ip.is_unspecified() && ip.segments()[0] & 0xffc0 != 0xfe80
|
|
290
|
+
}
|
|
291
|
+
std::net::IpAddr::V4(_) => false,
|
|
292
|
+
});
|
|
293
|
+
let mut addrs = vec![std::net::SocketAddr::from(([0, 0, 0, 0], 0))];
|
|
294
|
+
if has_ipv6 {
|
|
295
|
+
addrs.push(std::net::SocketAddr::from(([0u16; 8], 0)));
|
|
296
|
+
}
|
|
297
|
+
addrs
|
|
298
|
+
}
|
|
299
|
+
|
|
270
300
|
/// What the pacer lets go back to back: a few dozen packets. A 4K key frame is
|
|
271
301
|
/// hundreds of packets, and released at once it overflows the receiver's
|
|
272
302
|
/// socket buffer, which loses the frame and draws another key frame request —
|
|
@@ -447,7 +477,7 @@ impl PeerConnectionEventHandler for Handler {
|
|
|
447
477
|
}
|
|
448
478
|
|
|
449
479
|
async fn on_connection_state_change(&self, state: RTCPeerConnectionState) {
|
|
450
|
-
// desklink scout trace
|
|
480
|
+
// desklink scout trace: the engine's rtc-level
|
|
451
481
|
// transport state event, on the same microsecond clock as the vendored
|
|
452
482
|
// agent's `[ice ...]` lines, so the rtc Connected gate is separable.
|
|
453
483
|
if dl_trace_on() {
|
|
@@ -871,8 +901,8 @@ impl VideoPeer {
|
|
|
871
901
|
options
|
|
872
902
|
.ice_servers
|
|
873
903
|
.iter()
|
|
874
|
-
.map(|(
|
|
875
|
-
urls:
|
|
904
|
+
.map(|(urls, username, credential)| RTCIceServer {
|
|
905
|
+
urls: urls.clone(),
|
|
876
906
|
username: username.clone().unwrap_or_default(),
|
|
877
907
|
credential: credential.clone().unwrap_or_default(),
|
|
878
908
|
})
|
|
@@ -899,7 +929,7 @@ impl VideoPeer {
|
|
|
899
929
|
// An ephemeral port on every interface: ICE needs a socket to gather
|
|
900
930
|
// candidates from, and the peer needs no fixed port because the
|
|
901
931
|
// consumer's authenticated channel carries the candidates.
|
|
902
|
-
.with_udp_addrs(
|
|
932
|
+
.with_udp_addrs(udp_addrs())
|
|
903
933
|
.with_tcp_addrs(if options.loopback_tcp {
|
|
904
934
|
vec![std::net::SocketAddr::from(([127, 0, 0, 1], 0))]
|
|
905
935
|
} else {
|
|
@@ -1103,6 +1133,21 @@ impl VideoPeer {
|
|
|
1103
1133
|
self.connected.load(Ordering::SeqCst)
|
|
1104
1134
|
}
|
|
1105
1135
|
|
|
1136
|
+
/// Whether ICE selected a TCP candidate pair (the relay's ICE-TCP path).
|
|
1137
|
+
/// False until a pair is selected.
|
|
1138
|
+
pub async fn selected_pair_is_tcp(&self) -> bool {
|
|
1139
|
+
let Some(sender) = self.peer.get_senders().await.into_iter().next() else {
|
|
1140
|
+
return false;
|
|
1141
|
+
};
|
|
1142
|
+
let Ok(Some(dtls)) = sender.transport().await else {
|
|
1143
|
+
return false;
|
|
1144
|
+
};
|
|
1145
|
+
matches!(
|
|
1146
|
+
dtls.ice_transport().get_selected_candidate_pair().await,
|
|
1147
|
+
Ok(Some(pair)) if pair.local().protocol == RTCIceProtocol::Tcp
|
|
1148
|
+
)
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1106
1151
|
/// Stand in for a connected transport, for a pipeline under test.
|
|
1107
1152
|
#[cfg(test)]
|
|
1108
1153
|
pub fn assume_connected(&self) {
|
|
@@ -1145,6 +1190,12 @@ impl VideoPeer {
|
|
|
1145
1190
|
lock(&self.path).take()
|
|
1146
1191
|
}
|
|
1147
1192
|
|
|
1193
|
+
/// Stand in for transport-wide feedback that reports a path, for a pipeline under test.
|
|
1194
|
+
#[cfg(test)]
|
|
1195
|
+
pub fn report_path(&self, report: PathReport) {
|
|
1196
|
+
*lock(&self.path) = Some(report);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1148
1199
|
/// Take the pending "the far end needs a reference frame" request, if any.
|
|
1149
1200
|
pub fn take_keyframe_request(&self) -> bool {
|
|
1150
1201
|
self.wants_keyframe.swap(false, Ordering::SeqCst)
|
|
@@ -1258,7 +1309,7 @@ pub(crate) mod tests {
|
|
|
1258
1309
|
.with_interceptor_registry(registry)
|
|
1259
1310
|
.with_runtime(default_runtime().expect("a runtime"))
|
|
1260
1311
|
.with_handler(Arc::new(NoopHandler))
|
|
1261
|
-
.with_udp_addrs(
|
|
1312
|
+
.with_udp_addrs(udp_addrs())
|
|
1262
1313
|
.build()
|
|
1263
1314
|
.await
|
|
1264
1315
|
.expect("an answering peer");
|
|
@@ -1667,7 +1718,11 @@ pub(crate) mod tests {
|
|
|
1667
1718
|
}
|
|
1668
1719
|
}
|
|
1669
1720
|
|
|
1670
|
-
async fn media_receiver(
|
|
1721
|
+
async fn media_receiver(
|
|
1722
|
+
receiver: Arc<MediaReceiver>,
|
|
1723
|
+
udp: Vec<std::net::SocketAddr>,
|
|
1724
|
+
tcp: Vec<std::net::SocketAddr>,
|
|
1725
|
+
) -> Arc<dyn PeerConnection> {
|
|
1671
1726
|
let mut media_engine = MediaEngine::default();
|
|
1672
1727
|
media_engine
|
|
1673
1728
|
.register_codec(vp9_codec(), RtpCodecKind::Video)
|
|
@@ -1679,20 +1734,38 @@ pub(crate) mod tests {
|
|
|
1679
1734
|
.with_interceptor_registry(registry)
|
|
1680
1735
|
.with_runtime(default_runtime().expect("a runtime"))
|
|
1681
1736
|
.with_handler(receiver)
|
|
1682
|
-
.with_udp_addrs(
|
|
1737
|
+
.with_udp_addrs(udp)
|
|
1738
|
+
.with_tcp_addrs(tcp)
|
|
1683
1739
|
.build()
|
|
1684
1740
|
.await
|
|
1685
1741
|
.expect("a receiving peer");
|
|
1686
1742
|
Arc::new(peer)
|
|
1687
1743
|
}
|
|
1688
1744
|
|
|
1689
|
-
///
|
|
1690
|
-
///
|
|
1691
|
-
///
|
|
1692
|
-
|
|
1693
|
-
|
|
1745
|
+
/// An engine and a client receiving on `client_udp`, offered, answered and
|
|
1746
|
+
/// connected over ICE, with the client's RTP arrival times and the
|
|
1747
|
+
/// candidates the engine offered.
|
|
1748
|
+
pub(crate) struct Connected {
|
|
1749
|
+
pub(crate) engine: Arc<VideoPeer>,
|
|
1750
|
+
offer: String,
|
|
1751
|
+
pub(crate) client: Arc<dyn PeerConnection>,
|
|
1752
|
+
arrivals: Arc<Mutex<Vec<Instant>>>,
|
|
1753
|
+
offered: Arc<Mutex<Vec<String>>>,
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
/// `client_tcp` and `loopback_tcp` give a pair that runs over ICE-TCP: a
|
|
1757
|
+
/// client with no UDP candidates can only reach the engine over TCP.
|
|
1758
|
+
pub(crate) async fn connected(
|
|
1759
|
+
client_udp: Vec<std::net::SocketAddr>,
|
|
1760
|
+
client_tcp: Vec<std::net::SocketAddr>,
|
|
1761
|
+
loopback_tcp: bool,
|
|
1762
|
+
) -> Connected {
|
|
1694
1763
|
let (events, mut events_rx) = tokio::sync::mpsc::unbounded_channel();
|
|
1695
|
-
let
|
|
1764
|
+
let options = TransportOptions {
|
|
1765
|
+
loopback_tcp,
|
|
1766
|
+
..transport()
|
|
1767
|
+
};
|
|
1768
|
+
let (engine, offer) = VideoPeer::offer(options, events, VideoCodec::Vp9)
|
|
1696
1769
|
.await
|
|
1697
1770
|
.expect("an offering peer");
|
|
1698
1771
|
let engine = Arc::new(engine);
|
|
@@ -1706,7 +1779,7 @@ pub(crate) mod tests {
|
|
|
1706
1779
|
arrivals: Arc::clone(&arrivals),
|
|
1707
1780
|
connected: Arc::clone(&connected),
|
|
1708
1781
|
});
|
|
1709
|
-
let client = media_receiver(Arc::clone(&receiver)).await;
|
|
1782
|
+
let client = media_receiver(Arc::clone(&receiver), client_udp, client_tcp).await;
|
|
1710
1783
|
|
|
1711
1784
|
client
|
|
1712
1785
|
.set_remote_description(RTCSessionDescription::offer(offer.clone()).expect("an offer"))
|
|
@@ -1724,6 +1797,8 @@ pub(crate) mod tests {
|
|
|
1724
1797
|
|
|
1725
1798
|
// The engine's candidates reach the client.
|
|
1726
1799
|
let client_for_engine_candidates = Arc::clone(&client);
|
|
1800
|
+
let offered: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
|
1801
|
+
let offered_by_engine = Arc::clone(&offered);
|
|
1727
1802
|
tokio::spawn(async move {
|
|
1728
1803
|
while let Some(event) = events_rx.recv().await {
|
|
1729
1804
|
if let PeerEvent::Candidate {
|
|
@@ -1732,6 +1807,7 @@ pub(crate) mod tests {
|
|
|
1732
1807
|
sdp_m_line_index,
|
|
1733
1808
|
} = event
|
|
1734
1809
|
{
|
|
1810
|
+
offered_by_engine.lock().unwrap().push(candidate.clone());
|
|
1735
1811
|
let _ = client_for_engine_candidates
|
|
1736
1812
|
.add_ice_candidate(RTCIceCandidateInit {
|
|
1737
1813
|
candidate,
|
|
@@ -1757,9 +1833,40 @@ pub(crate) mod tests {
|
|
|
1757
1833
|
// Wait for the ICE path to come up on both ends.
|
|
1758
1834
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
1759
1835
|
while !(engine.is_connected() && connected.load(Ordering::SeqCst)) {
|
|
1760
|
-
assert!(
|
|
1836
|
+
assert!(
|
|
1837
|
+
Instant::now() < deadline,
|
|
1838
|
+
"the peers never connected; the engine offered {:?}",
|
|
1839
|
+
offered.lock().unwrap()
|
|
1840
|
+
);
|
|
1761
1841
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
1762
1842
|
}
|
|
1843
|
+
Connected {
|
|
1844
|
+
engine,
|
|
1845
|
+
offer,
|
|
1846
|
+
client,
|
|
1847
|
+
arrivals,
|
|
1848
|
+
offered,
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
/// A restart offer/answer round must not interrupt media already flowing on
|
|
1853
|
+
/// the selected pair: the picture keeps advancing (no gap over 200 ms) while
|
|
1854
|
+
/// the old pair stays reachable, and the ICE generation still turns over.
|
|
1855
|
+
#[tokio::test]
|
|
1856
|
+
async fn a_restart_keeps_media_flowing_on_the_selected_pair_until_the_new_one_nominates() {
|
|
1857
|
+
let Connected {
|
|
1858
|
+
engine,
|
|
1859
|
+
offer,
|
|
1860
|
+
client,
|
|
1861
|
+
arrivals,
|
|
1862
|
+
offered,
|
|
1863
|
+
} = connected(
|
|
1864
|
+
vec![std::net::SocketAddr::from(([0, 0, 0, 0], 0))],
|
|
1865
|
+
Vec::new(),
|
|
1866
|
+
false,
|
|
1867
|
+
)
|
|
1868
|
+
.await;
|
|
1869
|
+
eprintln!("restart: the engine offered {:?}", offered.lock().unwrap());
|
|
1763
1870
|
|
|
1764
1871
|
// Any bytes the VP9 packetizer turns into one access unit.
|
|
1765
1872
|
let frame: Vec<u8> = (0..200u32).map(|i| (i % 251) as u8).collect();
|
|
@@ -1886,6 +1993,65 @@ pub(crate) mod tests {
|
|
|
1886
1993
|
let _ = client.close().await;
|
|
1887
1994
|
}
|
|
1888
1995
|
|
|
1996
|
+
/// A phone whose only direct way here is IPv6, such as one on a carrier that
|
|
1997
|
+
/// hands it a global IPv6 address, still gets the picture: the engine offers
|
|
1998
|
+
/// its IPv6 addresses, never a loopback or link-local one, and a client that
|
|
1999
|
+
/// can only send from `::1` connects and receives media.
|
|
2000
|
+
#[tokio::test]
|
|
2001
|
+
async fn a_client_with_only_an_ipv6_route_gets_the_picture() {
|
|
2002
|
+
if !udp_addrs().iter().any(std::net::SocketAddr::is_ipv6) {
|
|
2003
|
+
eprintln!("ipv6: skipped, this computer has no IPv6 address a peer could reach");
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
let Connected {
|
|
2007
|
+
engine,
|
|
2008
|
+
client,
|
|
2009
|
+
arrivals,
|
|
2010
|
+
offered,
|
|
2011
|
+
..
|
|
2012
|
+
} = connected(
|
|
2013
|
+
vec![std::net::SocketAddr::from((
|
|
2014
|
+
std::net::Ipv6Addr::LOCALHOST,
|
|
2015
|
+
0,
|
|
2016
|
+
))],
|
|
2017
|
+
Vec::new(),
|
|
2018
|
+
false,
|
|
2019
|
+
)
|
|
2020
|
+
.await;
|
|
2021
|
+
let offered = offered.lock().unwrap().clone();
|
|
2022
|
+
eprintln!("ipv6: the engine offered {offered:?}");
|
|
2023
|
+
let ipv6: Vec<std::net::Ipv6Addr> = offered
|
|
2024
|
+
.iter()
|
|
2025
|
+
.filter_map(|candidate| candidate.split(' ').nth(4)?.parse().ok())
|
|
2026
|
+
.collect();
|
|
2027
|
+
assert!(!ipv6.is_empty(), "no IPv6 candidate was offered");
|
|
2028
|
+
assert!(
|
|
2029
|
+
!ipv6
|
|
2030
|
+
.iter()
|
|
2031
|
+
.any(|ip| ip.is_loopback() || ip.segments()[0] & 0xffc0 == 0xfe80),
|
|
2032
|
+
"a loopback or link-local address reached the client: {ipv6:?}"
|
|
2033
|
+
);
|
|
2034
|
+
|
|
2035
|
+
let frame: Vec<u8> = (0..200u32).map(|i| (i % 251) as u8).collect();
|
|
2036
|
+
for _ in 0..50 {
|
|
2037
|
+
engine
|
|
2038
|
+
.send_frame(&frame, true, Instant::now())
|
|
2039
|
+
.await
|
|
2040
|
+
.expect("a frame is sent");
|
|
2041
|
+
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
2042
|
+
}
|
|
2043
|
+
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
2044
|
+
let received = arrivals.lock().unwrap().len();
|
|
2045
|
+
eprintln!("ipv6: {received} of 50 frames arrived at a client on ::1");
|
|
2046
|
+
assert!(
|
|
2047
|
+
received >= 25,
|
|
2048
|
+
"the picture did not arrive over IPv6: {received} packets"
|
|
2049
|
+
);
|
|
2050
|
+
|
|
2051
|
+
engine.close().await;
|
|
2052
|
+
let _ = client.close().await;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
1889
2055
|
#[tokio::test]
|
|
1890
2056
|
async fn a_forwarded_client_is_offered_ice_over_tcp_on_the_loopback() {
|
|
1891
2057
|
let offered = |loopback_tcp: bool| async move {
|
|
@@ -1926,4 +2092,110 @@ pub(crate) mod tests {
|
|
|
1926
2092
|
"no one else is offered a port on the loopback"
|
|
1927
2093
|
);
|
|
1928
2094
|
}
|
|
2095
|
+
|
|
2096
|
+
/// A STUN responder bound on the wildcard that answers every Binding
|
|
2097
|
+
/// request with the requester's own address, the way a real STUN server
|
|
2098
|
+
/// would. Reach it at the address `primary_ip` names, not at its bind
|
|
2099
|
+
/// address: the wildcard receives for every interface.
|
|
2100
|
+
async fn stun_responder() -> std::net::SocketAddr {
|
|
2101
|
+
use rtc::stun::message::Setter;
|
|
2102
|
+
let socket = tokio::net::UdpSocket::bind("0.0.0.0:0")
|
|
2103
|
+
.await
|
|
2104
|
+
.expect("a socket");
|
|
2105
|
+
let addr = socket.local_addr().expect("a bound port");
|
|
2106
|
+
tokio::spawn(async move {
|
|
2107
|
+
let mut buffer = vec![0u8; 1500];
|
|
2108
|
+
while let Ok((size, peer)) = socket.recv_from(&mut buffer).await {
|
|
2109
|
+
let mut request = rtc::stun::message::Message::new();
|
|
2110
|
+
request.raw = buffer[..size].to_vec();
|
|
2111
|
+
if request.decode().is_err() {
|
|
2112
|
+
continue;
|
|
2113
|
+
}
|
|
2114
|
+
let mut response = rtc::stun::message::Message::new();
|
|
2115
|
+
response.set_type(rtc::stun::message::BINDING_SUCCESS);
|
|
2116
|
+
response.transaction_id = request.transaction_id;
|
|
2117
|
+
let mapped = rtc::stun::xoraddr::XorMappedAddress {
|
|
2118
|
+
ip: peer.ip(),
|
|
2119
|
+
port: peer.port(),
|
|
2120
|
+
};
|
|
2121
|
+
if mapped.add_to(&mut response).is_err() {
|
|
2122
|
+
continue;
|
|
2123
|
+
}
|
|
2124
|
+
response.write_header();
|
|
2125
|
+
let _ = socket.send_to(&response.raw, peer).await;
|
|
2126
|
+
}
|
|
2127
|
+
});
|
|
2128
|
+
addr
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
/// The interface address this machine sources its own traffic from — the
|
|
2132
|
+
/// address a socket bound to this machine's interfaces can reach. The
|
|
2133
|
+
/// peer binds one socket per non-loopback interface (loopback is skipped
|
|
2134
|
+
/// when host candidates are gathered), and Windows' strong-host model
|
|
2135
|
+
/// refuses to send an interface-bound datagram to 127.0.0.1, where the
|
|
2136
|
+
/// Linux weak-host model allows it — so a loopback STUN fixture gathers
|
|
2137
|
+
/// nothing on Windows. A real STUN server sits on the subnet, and the
|
|
2138
|
+
/// fixture does the same. The probe is a connected UDP socket: the OS
|
|
2139
|
+
/// resolves the route and its source address without sending a packet.
|
|
2140
|
+
fn primary_ip() -> std::net::IpAddr {
|
|
2141
|
+
let probe = std::net::UdpSocket::bind("0.0.0.0:0").and_then(|probe| {
|
|
2142
|
+
probe.connect("192.0.2.1:9")?; // TEST-NET-1: connected UDP sends nothing
|
|
2143
|
+
probe.local_addr()
|
|
2144
|
+
});
|
|
2145
|
+
match probe {
|
|
2146
|
+
Ok(addr) if !addr.ip().is_unspecified() && !addr.ip().is_loopback() => addr.ip(),
|
|
2147
|
+
_ => std::net::IpAddr::from([127, 0, 0, 1]),
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
/// One ice_servers entry listing two URLs must put both in front of the
|
|
2152
|
+
/// peer connection: the first responder here never answers, so any
|
|
2153
|
+
/// reflexive candidate can only have come from the second URL. The engine
|
|
2154
|
+
/// used to forward only the first URL of each entry, which lost the rest.
|
|
2155
|
+
#[tokio::test]
|
|
2156
|
+
async fn the_second_url_of_an_ice_server_entry_gathers_a_reflexive_candidate() {
|
|
2157
|
+
let ip = primary_ip();
|
|
2158
|
+
// A server that never answers: bound, so its port cannot be reused,
|
|
2159
|
+
// and silent, so it is as good as unreachable.
|
|
2160
|
+
let silent = tokio::net::UdpSocket::bind("0.0.0.0:0")
|
|
2161
|
+
.await
|
|
2162
|
+
.expect("a socket");
|
|
2163
|
+
let unreachable = silent.local_addr().expect("a port");
|
|
2164
|
+
let answering = stun_responder().await;
|
|
2165
|
+
|
|
2166
|
+
let (events, mut events_rx) = tokio::sync::mpsc::unbounded_channel();
|
|
2167
|
+
let (_peer, _offer) = VideoPeer::offer(
|
|
2168
|
+
TransportOptions {
|
|
2169
|
+
ice_servers: vec![(
|
|
2170
|
+
vec![
|
|
2171
|
+
format!("stun:{ip}:{}", unreachable.port()),
|
|
2172
|
+
format!("stun:{ip}:{}", answering.port()),
|
|
2173
|
+
],
|
|
2174
|
+
None,
|
|
2175
|
+
None,
|
|
2176
|
+
)],
|
|
2177
|
+
loopback_tcp: false,
|
|
2178
|
+
pace_bps: 20_000_000.0,
|
|
2179
|
+
},
|
|
2180
|
+
events,
|
|
2181
|
+
VideoCodec::Vp9,
|
|
2182
|
+
)
|
|
2183
|
+
.await
|
|
2184
|
+
.expect("a peer connection");
|
|
2185
|
+
|
|
2186
|
+
// Host candidates arrive without the network; the reflexive one needs
|
|
2187
|
+
// the binding round trip through the second URL of the entry.
|
|
2188
|
+
let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
|
|
2189
|
+
loop {
|
|
2190
|
+
let event = tokio::time::timeout_at(deadline, events_rx.recv())
|
|
2191
|
+
.await
|
|
2192
|
+
.expect("no reflexive candidate was ever gathered")
|
|
2193
|
+
.expect("the peer event stream ended");
|
|
2194
|
+
if let PeerEvent::Candidate { candidate, .. } = event {
|
|
2195
|
+
if candidate.contains(" typ srflx ") {
|
|
2196
|
+
break;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
1929
2201
|
}
|