This is a continuation of my previous question here.
I started using the webrtc
crate in this library to create a WebRTC connection, but I'm having issues with the ICE connection failing. I checked the STUN and TURN servers being used with this site and they all work correctly.
Here is my current code:
async fn new(...) {
let webrtcredux = Arc::new(AsyncMutex::new(WebRtcRedux::default()));
webrtcredux.lock().await.set_tokio_runtime(Handle::current());
let servers = ice.urls.into_iter().map(|url| {
if url.starts_with("turn") {
RTCIceServer {
urls: vec![url],
username: ice.username.clone(),
credential: ice.credential.clone(),
.. RTCIceServer::default()
}
} else {
RTCIceServer {
urls: vec![url],
.. RTCIceServer::default()
}
}
}).collect::<Vec<_>>();
debug!("Using ICE servers: {:#?}", servers);
webrtcredux.lock().await.add_ice_servers(servers);
// More gstreamer setup code...
}
async fn start(...) {
self.pipeline.set_state(gst::State::Playing)?;
let encoder = self.encoder_type;
let arc_from_ws = Arc::new(AsyncMutex::new(from_ws_rx));
self.webrtcredux.lock().await.on_peer_connection_state_change(Box::new(|state| {
debug!("[WebRTC] Peer connection state changed to: {}", state);
Box::pin(async {})
})).await.expect("Failed to set on peer connection state change");
self.webrtcredux.lock().await.on_ice_connection_state_change(Box::new(|state| {
debug!("[WebRTC] ICE connection state changed to: {}", state);
Box::pin(async {})
})).await.expect("Failed to set on ice connection state change");
// let redux_arc = self.webrtcredux.clone();
// let candidates = Arc::new(AsyncMutex::new(Vec::new()));
// let candidates_arc = candidates.clone();
self.webrtcredux.lock().await.on_ice_candidate(Box::new(move |candidate| {
// let redux_arc = redux_arc.clone()
// let candidates = candidates_arc.clone();
Box::pin(async move {
if let Some(candidate) = candidate {
debug!("ICE Candidate: {:#?}", candidate.to_json().await.unwrap());
// candidates.lock().await.push(candidate.to_json().await.unwrap());
}
// redux_arc.lock().await.add_ice_candidate(candidate.unwrap().to_json().await.unwrap()).await.unwrap();
})
})).await.expect("Failed ice candidate");
let redux_arc = self.webrtcredux.clone();
self.webrtcredux.lock().await.on_negotiation_needed(Box::new(move || {
let redux_arc = redux_arc.clone();
info!("[WebRTC] Negotiation needed");
Box::pin(async move {
// Waits for all tracks to be added to create full SDP
redux_arc.lock().await.wait_for_all_tracks().await;
let offer = redux_arc.lock().await.create_offer(Some(RTCOfferOptions {
voice_activity_detection: true,
ice_restart: false,
})).await.expect("Failed to create offer");
// offer.props.insert(4, SdpProp::Attribute {
// key: "ice-options".to_string(),
// value: Some("trickle".to_string())
// });
// offer.props.insert(5, SdpProp::Attribute {
// key: "extmap-allow-mixed".to_string(),
// value: None
// });
// offer.props.insert(6, SdpProp::Attribute {
// key: "msid-semantic".to_string(),
// value: Some(" WMS".to_string())
// });
trace!("[WebRTC] Generated local SDP: {}", offer.to_string());
redux_arc.lock().await.set_local_description(&offer, RTCSdpType::Offer).await.expect("Failed to set local description");
info!("[WebRTC] Local description set");
})
})).await.expect("Failed to set on negotiation needed");
let redux_arc = self.webrtcredux.clone();
self.webrtcredux.lock().await.on_ice_gathering_state_change(Box::new(move |state| {
debug!("[WebRTC] ICE gathering state changed to: {}", state);
let redux_arc = redux_arc.clone();
let to_ws_tx = to_ws_tx.clone();
let from_ws_rx = arc_from_ws.clone();
if state != RTCIceGathererState::Complete {
return Box::pin(async {});
}
Box::pin(async move {
let local = redux_arc.lock().await.local_description().await.unwrap().unwrap();
let video_media: &SdpProp = local.props.iter().find(|v| match *v {
SdpProp::Media { r#type, .. } => {
*r#type == MediaType::Video
},
_ => false
}).unwrap();
let (video_ssrc, video_payload_type, rtx_payload_type) = if let SdpProp::Media { props, .. } = video_media {
let mut ssrc = 0u32;
let mut video_payload = 0u8;
let mut rtx_payload = 0u8;
for prop in props {
match prop {
MediaProp::Attribute { key, value } => {
match key {
v if *v == "rtpmap".to_string() => {
match value {
Some(val) => {
let num = val.clone().split(' ').collect::<Vec<_>>()[0].parse::<u8>().unwrap();
if val.ends_with(&format!("{}/90000", encoder.type_string())) && video_payload == 0 {
video_payload = num;
} else if val.ends_with("rtx/90000") && rtx_payload == 0 {
rtx_payload = num;
}
},
None => unreachable!()
}
},
v if *v == "ssrc".to_string() => {
ssrc = match value {
Some(val) => val.clone().split(' ').collect::<Vec<_>>()[0].parse::<u32>().unwrap(),
None => unreachable!(),
};
},
_ => continue
}
},
_ => continue
}
}
(ssrc, video_payload, rtx_payload)
} else { unreachable!() };
let audio_media: &SdpProp = local.props.iter().find(|v| match *v {
SdpProp::Media { r#type, .. } => {
*r#type == MediaType::Audio
},
_ => false
}).unwrap();
let audio_ssrc = if let SdpProp::Media { props, .. } = audio_media {
props.into_iter().find_map(|p| match p {
MediaProp::Attribute {key, value} => {
if key != "ssrc" {
return None;
}
let val = match value {
Some(val) => val.clone(),
None => unreachable!(),
};
Some(val.split(' ').collect::<Vec<_>>()[0].parse::<u32>().unwrap())
},
_ => None
}).unwrap()
} else { unreachable!() };
trace!("[WebRTC] Updated local SDP: {}", local.to_string());
to_ws_tx.send(ToWs {
ssrcs: StreamSSRCs {
audio: audio_ssrc,
video: video_ssrc,
rtx: 0
},
local_sdp: local.to_string(),
video_payload_type,
rtx_payload_type,
}).await.unwrap();
let from_ws = from_ws_rx.lock().await.recv().await.unwrap();
match SDP::from_str(&from_ws.remote_sdp).unwrap().props.pop().unwrap() {
SdpProp::Media { ports, props, .. } => {
let mut main_ip = None;
let mut fingerprint = None;
let mut ufrag = None;
let mut pwd = None;
let mut candidate = None;
for prop in props {
let current = prop.clone();
match prop {
MediaProp::Connection { address, .. } => main_ip = Some(address),
MediaProp::Attribute { key, value: _ } => {
match &key[..] {
"candidate" => candidate = Some(current),
"fingerprint" => fingerprint = Some(current),
"ice-ufrag" => ufrag = Some(current),
"ice-pwd" => pwd = Some(current),
_ => continue
}
}
_ => continue
}
}
let connection = MediaProp::Connection {
net_type: NetworkType::Internet,
address_type: AddressType::IPv4,
address: main_ip.unwrap(),
ttl: Some(127),
num_addresses: Some(1),
suffix: None,
};
let base_media_props = vec![
connection,
// candidate.unwrap(),
fingerprint.unwrap(),
ufrag.unwrap(),
pwd.unwrap(),
MediaProp::Attribute {
key: "rtcp-mux".to_string(),
value: None
},
MediaProp::Attribute {
key: "rtcp".to_string(),
value: Some(ports[0].to_string())
},
MediaProp::Attribute {
key: "setup".to_string(),
value: Some("passive".to_string())
},
MediaProp::Attribute {
key: "inactive".to_string(),
value: None
}
];
let mut video_vec_attrs = ["ccm fir", "nack", "nack pli", "goog-remb", "transport-cc"].into_iter().map(|val| {
MediaProp::Attribute {
key: "rtcp-fb".to_string(),
value: Some(format!("{} {}", video_payload_type, val))
}
}).collect::<Vec<_>>();
video_vec_attrs.append(&mut [
"2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time",
"3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01",
"14 urn:ietf:params:rtp-hdrext:toffset",
"13 urn:3gpp:video-orientation",
"5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay"
].into_iter().map(|ext| {
MediaProp::Attribute {
key: "extmap".to_string(),
value: Some(ext.to_string())
}
}).collect::<Vec<_>>());
video_vec_attrs.append(&mut vec![
MediaProp::Attribute {
key: "fmtp".to_string(),
value: Some(format!("{} x-google-max-bitrate=2500;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", video_payload_type))
},
MediaProp::Attribute {
key: "fmtp".to_string(),
value: Some(format!("{} apt={}", rtx_payload_type, video_payload_type))
},
MediaProp::Attribute {
key: "mid".to_string(),
value: Some(0.to_string())
},
MediaProp::Attribute {
key: "rtpmap".to_string(),
value: Some(format!("{} {}/90000", video_payload_type, encoder.type_string()))
},
MediaProp::Attribute {
key: "rtpmap".to_string(),
value: Some(format!("{} rtx/90000", rtx_payload_type))
},
candidate.unwrap(),
MediaProp::Attribute {
key: "end-of-candidates".to_string(),
value: None
}
]);
let video_media = SdpProp::Media {
r#type: MediaType::Video,
ports: ports.clone(),
protocol: format!("UDP/TLS/RTP/SAVPF {} {}", video_payload_type, rtx_payload_type),
format: "".to_string(),
props: base_media_props.clone().into_iter().chain(video_vec_attrs.into_iter()).collect::<Vec<_>>()
};
let mut audio_vec_attrs = [
"1 urn:ietf:params:rtp-hdrext:ssrc-audio-level",
"3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"
].into_iter().map(|ext| {
MediaProp::Attribute {
key: "extmap".to_string(),
value: Some(ext.to_string())
}
}).collect::<Vec<_>>();
audio_vec_attrs.append(&mut vec![
MediaProp::Attribute {
key: "fmtp".to_string(),
value: Some("111 minptime=10;useinbandfec=1;usedtx=1".to_string())
},
MediaProp::Attribute {
key: "maxptime".to_string(),
value: Some(60.to_string())
},
MediaProp::Attribute {
key: "rtpmap".to_string(),
value: Some("111 opus/90000".to_string())
},
MediaProp::Attribute {
key: "rtcp-fb".to_string(),
value: Some("111 transport-cc".to_string())
},
MediaProp::Attribute {
key: "mid".to_string(),
value: Some(1.to_string())
}
]);
let audio_media = SdpProp::Media {
r#type: MediaType::Audio,
ports,
protocol: "UDP/TLS/RTP/SAVPF 111".to_string(),
format: "".to_string(),
props: base_media_props.clone().into_iter().chain(audio_vec_attrs.into_iter()).collect::<Vec<_>>()
};
// Generate answer
let answer = SDP { props: vec![
SdpProp::Version(0),
SdpProp::Origin {
username: "-".to_string(),
session_id: "1420070400000".to_string(),
session_version: 0,
net_type: NetworkType::Internet,
address_type: AddressType::IPv4,
address: "127.0.0.1".to_string()
},
SdpProp::SessionName("-".to_string()),
SdpProp::Timing {
start: 0,
stop: 0
},
SdpProp::Attribute {
key: "msid-semantic".to_string(),
value: Some(" WMS *".to_string())
},
SdpProp::Attribute {
key: "group".to_string(),
value: Some("BUNDLE 0 1".to_string())
},
video_media,
audio_media
]};
trace!("[WebRTC] Generated remote SDP: {}", answer.to_string());
redux_arc.lock().await.set_remote_description(&answer, RTCSdpType::Answer).await.expect("Failed to set remote description");
info!("[WebRTC] Remote description set");
}
_ => unreachable!()
}
})
})).await.expect("Failed to set on ice gathering change");
Ok(StateChangeSuccess::Success)
}
Local SDP after ICE gathering completes:
v=0
o=- 3006164469565782471 253007078 IN IP4 0.0.0.0
s=-
t=0 0
a=fingerprint:sha-256 F5:34:75:08:3E:AB:99:1E:5F:79:BF:6D:14:EC:D6:C2:F6:20:74:D6:D3:1D:78:48:58:B6:1E:2B:32:F3:D9:64
a=group:BUNDLE 0 1
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 123 118 116
c=IN IP4 0.0.0.0
a=setup:actpass
a=mid:0
a=ice-ufrag:cWRCBPTiuOohkLsf
a=ice-pwd:mHMqXcRexKOkbHKAZlvxjgvLFtdHiZAL
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtcp-fb:96 transport-cc
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtcp-fb:97 nack
a=rtcp-fb:97 nack pli
a=rtcp-fb:97 transport-cc
a=rtpmap:98 VP9/90000
a=fmtp:98 profile-id=0
a=rtcp-fb:98 goog-remb
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=rtcp-fb:98 transport-cc
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtcp-fb:99 nack
a=rtcp-fb:99 nack pli
a=rtcp-fb:99 transport-cc
a=rtpmap:100 VP9/90000
a=fmtp:100 profile-id=1
a=rtcp-fb:100 goog-remb
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=rtcp-fb:100 transport-cc
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtcp-fb:101 nack
a=rtcp-fb:101 nack pli
a=rtcp-fb:101 transport-cc
a=rtpmap:102 H264/90000
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtcp-fb:102 goog-remb
a=rtcp-fb:102 ccm fir
a=rtcp-fb:102 nack
a=rtcp-fb:102 nack pli
a=rtcp-fb:102 nack
a=rtcp-fb:102 nack pli
a=rtcp-fb:102 transport-cc
a=rtpmap:121 rtx/90000
a=fmtp:121 apt=102
a=rtcp-fb:121 nack
a=rtcp-fb:121 nack pli
a=rtcp-fb:121 transport-cc
a=rtpmap:127 H264/90000
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=rtcp-fb:127 transport-cc
a=rtpmap:120 rtx/90000
a=fmtp:120 apt=127
a=rtcp-fb:120 nack
a=rtcp-fb:120 nack pli
a=rtcp-fb:120 transport-cc
a=rtpmap:125 H264/90000
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtcp-fb:125 goog-remb
a=rtcp-fb:125 ccm fir
a=rtcp-fb:125 nack
a=rtcp-fb:125 nack pli
a=rtcp-fb:125 nack
a=rtcp-fb:125 nack pli
a=rtcp-fb:125 transport-cc
a=rtpmap:107 rtx/90000
a=fmtp:107 apt=125
a=rtcp-fb:107 nack
a=rtcp-fb:107 nack pli
a=rtcp-fb:107 transport-cc
a=rtpmap:108 H264/90000
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
a=rtcp-fb:108 goog-remb
a=rtcp-fb:108 ccm fir
a=rtcp-fb:108 nack
a=rtcp-fb:108 nack pli
a=rtcp-fb:108 nack
a=rtcp-fb:108 nack pli
a=rtcp-fb:108 transport-cc
a=rtpmap:109 rtx/90000
a=fmtp:109 apt=108
a=rtcp-fb:109 nack
a=rtcp-fb:109 nack pli
a=rtcp-fb:109 transport-cc
a=rtpmap:123 H264/90000
a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032
a=rtcp-fb:123 goog-remb
a=rtcp-fb:123 ccm fir
a=rtcp-fb:123 nack
a=rtcp-fb:123 nack pli
a=rtcp-fb:123 nack
a=rtcp-fb:123 nack pli
a=rtcp-fb:123 transport-cc
a=rtpmap:118 rtx/90000
a=fmtp:118 apt=123
a=rtcp-fb:118 nack
a=rtcp-fb:118 nack pli
a=rtcp-fb:118 transport-cc
a=rtpmap:116 ulpfec/90000
a=rtcp-fb:116 nack
a=rtcp-fb:116 nack pli
a=rtcp-fb:116 transport-cc
a=extmap:1 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=ssrc:3980097584 cname:video_0
a=ssrc:3980097584 msid:video_0 video
a=ssrc:3980097584 mslabel:video_0
a=ssrc:3980097584 label:video
a=msid:video_0 video
a=sendrecv
a=candidate:167090039 1 udp 2130706431 :: 48818 typ host
a=candidate:167090039 2 udp 2130706431 :: 48818 typ host
a=candidate:2938512866 1 udp 2130706431 192.168.1.100 47953 typ host
a=candidate:2938512866 2 udp 2130706431 192.168.1.100 47953 typ host
a=candidate:2414835526 1 udp 1694498815 72.196.215.130 35989 typ srflx raddr 0.0.0.0 rport 35989
a=candidate:2414835526 2 udp 1694498815 72.196.215.130 35989 typ srflx raddr 0.0.0.0 rport 35989
a=candidate:2414835526 1 udp 1694498815 72.196.215.130 37580 typ srflx raddr 0.0.0.0 rport 37580
a=candidate:2414835526 2 udp 1694498815 72.196.215.130 37580 typ srflx raddr 0.0.0.0 rport 37580
a=candidate:2414835526 1 udp 1694498815 72.196.215.130 59238 typ srflx raddr 0.0.0.0 rport 59238
a=candidate:2414835526 2 udp 1694498815 72.196.215.130 59238 typ srflx raddr 0.0.0.0 rport 59238
a=candidate:2414835526 1 udp 1694498815 72.196.215.130 53377 typ srflx raddr 0.0.0.0 rport 53377
a=candidate:2414835526 2 udp 1694498815 72.196.215.130 53377 typ srflx raddr 0.0.0.0 rport 53377
a=candidate:1022905401 1 udp 16777215 34.203.251.215 29290 typ relay raddr 0.0.0.0 rport 38594
a=candidate:1022905401 2 udp 16777215 34.203.251.215 29290 typ relay raddr 0.0.0.0 rport 38594
a=end-of-candidates
m=audio 9 UDP/TLS/RTP/SAVPF 111 9 0 8
c=IN IP4 0.0.0.0
a=setup:actpass
a=mid:1
a=ice-ufrag:cWRCBPTiuOohkLsf
a=ice-pwd:mHMqXcRexKOkbHKAZlvxjgvLFtdHiZAL
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1
a=rtcp-fb:111 transport-cc
a=rtpmap:9 G722/8000
a=rtcp-fb:9 transport-cc
a=rtpmap:0 PCMU/8000
a=rtcp-fb:0 transport-cc
a=rtpmap:8 PCMA/8000
a=rtcp-fb:8 transport-cc
a=extmap:1 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=ssrc:597106938 cname:audio_0
a=ssrc:597106938 msid:audio_0 audio
a=ssrc:597106938 mslabel:audio_0
a=ssrc:597106938 label:audio
a=msid:audio_0 audio
a=sendrecv
Generated remote SDP:
v=0
o=- 1420070400000 0 IN IP4 127.0.0.1
s=-
t=0 0
a=msid-semantic: WMS *
a=group:BUNDLE 0 1
m=video 50016 UDP/TLS/RTP/SAVPF 98 97
c=IN IP4 66.22.231.190/127/1
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=ice-ufrag:PkLE
a=ice-pwd:o9QGn2N6YizFOM/UNojYai
a=rtcp-mux
a=rtcp:50016
a=setup:passive
a=inactive
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=rtcp-fb:98 goog-remb
a=rtcp-fb:98 transport-cc
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:13 urn:3gpp:video-orientation
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=fmtp:98 x-google-max-bitrate=2500;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=fmtp:97 apt=98
a=mid:0
a=rtpmap:98 VP9/90000
a=rtpmap:97 rtx/90000
a=candidate:1 1 UDP 4261412862 66.22.231.190 50016 typ host
a=end-of-candidates
m=audio 50016 UDP/TLS/RTP/SAVPF 111
c=IN IP4 66.22.231.190/127/1
a=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87
a=ice-ufrag:PkLE
a=ice-pwd:o9QGn2N6YizFOM/UNojYai
a=rtcp-mux
a=rtcp:50016
a=setup:passive
a=inactive
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
a=maxptime:60
a=rtpmap:111 opus/90000
a=rtcp-fb:111 transport-cc
a=mid:1
After setting the remote answer SDP the ICE connection state changes to "checking", but after about 20 seconds it changes to "failing" and kills the connection. Is there something wrong I'm doing in terms of the SDPs or my code?
Edit: I got logging working, here is the connection log: https://pastebin.com/vNvd3Af6
Edit 2: I'm not receiving any inbound traffic from the STUN servers. Other programs using the same ICE servers work fine, so what could I be doing wrong outside of basic network configuration?
Edit 3: Here is a working ICE connection capture and here is the one I'm currently dealing with.
Edit 4: I ran netstat
to see what ports my code is listening on, and there are some differences. I cut out all other programs.
Here is the working ICE connection:
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 192.168.1.100:41383 0.0.0.0:* LISTEN 37973/target/debug/
tcp 0 0 192.168.1.100:51469 0.0.0.0:* LISTEN 37973/target/debug/
tcp6 0 0 fe80::60d2:bcaa:a:40899 :::* LISTEN 37973/target/debug/
udp 0 0 192.168.1.100:44897 0.0.0.0:* 37973/target/debug/
udp 0 0 239.255.255.250:1900 0.0.0.0:* 37973/target/debug/
udp 0 0 192.168.1.100:1900 0.0.0.0:* 37973/target/debug/
udp 0 0 239.255.255.250:1900 0.0.0.0:* 37973/target/debug/
udp 0 0 127.0.0.1:1900 0.0.0.0:* 37973/target/debug/
udp 0 0 127.0.0.1:37386 0.0.0.0:* 37973/target/debug/
udp 0 0 192.168.1.100:59877 0.0.0.0:* 37973/target/debug/
udp6 0 0 fe80::60d2:bcaa:a:56003 :::* 37973/target/debug/
Here is the non-working connection:
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
udp 0 0 0.0.0.0:50651 0.0.0.0:* 37186/target/debug/
udp 0 0 0.0.0.0:51996 0.0.0.0:* 37186/target/debug/
udp 0 0 0.0.0.0:35776 0.0.0.0:* 37186/target/debug/
udp 0 0 0.0.0.0:53036 0.0.0.0:* 37186/target/debug/
udp 0 0 224.0.0.251:5353 0.0.0.0:* 37186/target/debug/
udp 0 0 0.0.0.0:40115 0.0.0.0:* 37186/target/debug/
udp 0 0 192.168.1.100:40707 0.0.0.0:* 37186/target/debug/
udp6 0 0 :::37965 :::* 37186/target/debug/
The server I was communicating with only accepted LF line endings, while I was sending CRLF in my new implementation. Changing it to LF fixed the issue.