fix ice candidate sending when different credentials are used

This commit is contained in:
Daniel Gultsch 2022-02-25 17:26:36 +01:00
parent 1f772df74f
commit 372078629b
3 changed files with 117 additions and 55 deletions

View file

@ -33,8 +33,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Queue; import java.util.Queue;
import java.util.Set; import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -349,16 +347,16 @@ public class JingleRtpConnection extends AbstractJingleConnection
private boolean checkForIceRestart( private boolean checkForIceRestart(
final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) { final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) {
final RtpContentMap existing = getRemoteContentMap(); final RtpContentMap existing = getRemoteContentMap();
final IceUdpTransportInfo.Credentials existingCredentials; final Set<IceUdpTransportInfo.Credentials> existingCredentials;
final IceUdpTransportInfo.Credentials newCredentials; final IceUdpTransportInfo.Credentials newCredentials;
try { try {
existingCredentials = existing.getCredentials(); existingCredentials = existing.getCredentials();
newCredentials = rtpContentMap.getCredentials(); newCredentials = rtpContentMap.getDistinctCredentials();
} catch (final IllegalStateException e) { } catch (final IllegalStateException e) {
Log.d(Config.LOGTAG, "unable to gather credentials for comparison", e); Log.d(Config.LOGTAG, "unable to gather credentials for comparison", e);
return false; return false;
} }
if (existingCredentials.equals(newCredentials)) { if (existingCredentials.contains(newCredentials)) {
return false; return false;
} }
// TODO an alternative approach is to check if we already got an iq result to our // TODO an alternative approach is to check if we already got an iq result to our
@ -1849,9 +1847,16 @@ public class JingleRtpConnection extends AbstractJingleConnection
public void onIceCandidate(final IceCandidate iceCandidate) { public void onIceCandidate(final IceCandidate iceCandidate) {
final RtpContentMap rtpContentMap = final RtpContentMap rtpContentMap =
isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap;
final String ufrag = rtpContentMap.getCredentials().ufrag; final IceUdpTransportInfo.Credentials credentials;
try {
credentials = rtpContentMap.getCredentials(iceCandidate.sdpMid);
} catch (final IllegalArgumentException e) {
Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate, e);
return;
}
final String uFrag = credentials.ufrag;
final IceUdpTransportInfo.Candidate candidate = final IceUdpTransportInfo.Candidate candidate =
IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, ufrag); IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, uFrag);
if (candidate == null) { if (candidate == null) {
Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate); Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate);
return; return;

View file

@ -16,7 +16,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableDecl;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
@ -39,7 +38,8 @@ public class RtpContentMap {
} }
public static RtpContentMap of(final JinglePacket jinglePacket) { public static RtpContentMap of(final JinglePacket jinglePacket) {
final Map<String, DescriptionTransport> contents = DescriptionTransport.of(jinglePacket.getJingleContents()); final Map<String, DescriptionTransport> contents =
DescriptionTransport.of(jinglePacket.getJingleContents());
if (isOmemoVerified(contents)) { if (isOmemoVerified(contents)) {
return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents); return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents);
} else { } else {
@ -62,21 +62,29 @@ public class RtpContentMap {
} }
public static RtpContentMap of(final SessionDescription sessionDescription) { public static RtpContentMap of(final SessionDescription sessionDescription) {
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder = new ImmutableMap.Builder<>(); final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
new ImmutableMap.Builder<>();
for (SessionDescription.Media media : sessionDescription.media) { for (SessionDescription.Media media : sessionDescription.media) {
final String id = Iterables.getFirst(media.attributes.get("mid"), null); final String id = Iterables.getFirst(media.attributes.get("mid"), null);
Preconditions.checkNotNull(id, "media has no mid"); Preconditions.checkNotNull(id, "media has no mid");
contentMapBuilder.put(id, DescriptionTransport.of(sessionDescription, media)); contentMapBuilder.put(id, DescriptionTransport.of(sessionDescription, media));
} }
final String groupAttribute = Iterables.getFirst(sessionDescription.attributes.get("group"), null); final String groupAttribute =
Iterables.getFirst(sessionDescription.attributes.get("group"), null);
final Group group = groupAttribute == null ? null : Group.ofSdpString(groupAttribute); final Group group = groupAttribute == null ? null : Group.ofSdpString(groupAttribute);
return new RtpContentMap(group, contentMapBuilder.build()); return new RtpContentMap(group, contentMapBuilder.build());
} }
public Set<Media> getMedia() { public Set<Media> getMedia() {
return Sets.newHashSet(Collections2.transform(contents.values(), input -> { return Sets.newHashSet(
final RtpDescription rtpDescription = input == null ? null : input.description; Collections2.transform(
return rtpDescription == null ? Media.UNKNOWN : input.description.getMedia(); contents.values(),
input -> {
final RtpDescription rtpDescription =
input == null ? null : input.description;
return rtpDescription == null
? Media.UNKNOWN
: input.description.getMedia();
})); }));
} }
@ -90,7 +98,8 @@ public class RtpContentMap {
} }
for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) { for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
if (entry.getValue().description == null) { if (entry.getValue().description == null) {
throw new IllegalStateException(String.format("%s is lacking content description", entry.getKey())); throw new IllegalStateException(
String.format("%s is lacking content description", entry.getKey()));
} }
} }
} }
@ -106,15 +115,24 @@ public class RtpContentMap {
for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) { for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
final IceUdpTransportInfo transport = entry.getValue().transport; final IceUdpTransportInfo transport = entry.getValue().transport;
final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
if (fingerprint == null || Strings.isNullOrEmpty(fingerprint.getContent()) || Strings.isNullOrEmpty(fingerprint.getHash())) { if (fingerprint == null
throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s", entry.getKey())); || Strings.isNullOrEmpty(fingerprint.getContent())
|| Strings.isNullOrEmpty(fingerprint.getHash())) {
throw new SecurityException(
String.format(
"Use of DTLS-SRTP (XEP-0320) is required for content %s",
entry.getKey()));
} }
final IceUdpTransportInfo.Setup setup = fingerprint.getSetup(); final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
if (setup == null) { if (setup == null) {
throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", entry.getKey())); throw new SecurityException(
String.format(
"Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute",
entry.getKey()));
} }
if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) { if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) {
throw new SecurityException("Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)"); throw new SecurityException(
"Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)");
} }
} }
} }
@ -135,41 +153,66 @@ public class RtpContentMap {
return jinglePacket; return jinglePacket;
} }
RtpContentMap transportInfo(final String contentName, final IceUdpTransportInfo.Candidate candidate) { RtpContentMap transportInfo(
final String contentName, final IceUdpTransportInfo.Candidate candidate) {
final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName); final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName);
final IceUdpTransportInfo transportInfo = descriptionTransport == null ? null : descriptionTransport.transport; final IceUdpTransportInfo transportInfo =
descriptionTransport == null ? null : descriptionTransport.transport;
if (transportInfo == null) { if (transportInfo == null) {
throw new IllegalArgumentException("Unable to find transport info for content name " + contentName); throw new IllegalArgumentException(
"Unable to find transport info for content name " + contentName);
} }
final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper(); final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper();
newTransportInfo.addChild(candidate); newTransportInfo.addChild(candidate);
return new RtpContentMap(null, ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo))); return new RtpContentMap(
null,
ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo)));
} }
RtpContentMap transportInfo() { RtpContentMap transportInfo() {
return new RtpContentMap( return new RtpContentMap(
null, null,
Maps.transformValues(contents, dt -> new DescriptionTransport(null, dt.transport.cloneWrapper())) Maps.transformValues(
); contents,
dt -> new DescriptionTransport(null, dt.transport.cloneWrapper())));
} }
public IceUdpTransportInfo.Credentials getCredentials() { public IceUdpTransportInfo.Credentials getDistinctCredentials() {
final Set<IceUdpTransportInfo.Credentials> allCredentials = ImmutableSet.copyOf(Collections2.transform( final Set<IceUdpTransportInfo.Credentials> allCredentials = getCredentials();
contents.values(), final IceUdpTransportInfo.Credentials credentials =
dt -> dt.transport.getCredentials() Iterables.getFirst(allCredentials, null);
));
final IceUdpTransportInfo.Credentials credentials = Iterables.getFirst(allCredentials, null);
if (allCredentials.size() == 1 && credentials != null) { if (allCredentials.size() == 1 && credentials != null) {
return credentials; return credentials;
} }
throw new IllegalStateException("Content map does not have distinct credentials"); throw new IllegalStateException("Content map does not have distinct credentials");
} }
public Set<IceUdpTransportInfo.Credentials> getCredentials() {
final Set<IceUdpTransportInfo.Credentials> credentials =
ImmutableSet.copyOf(
Collections2.transform(
contents.values(), dt -> dt.transport.getCredentials()));
if (credentials.isEmpty()) {
throw new IllegalStateException("Content map does not have any credentials");
}
return credentials;
}
public IceUdpTransportInfo.Credentials getCredentials(final String contentName) {
final DescriptionTransport descriptionTransport = this.contents.get(contentName);
if (descriptionTransport == null) {
throw new IllegalArgumentException(
String.format(
"Unable to find transport info for content name %s", contentName));
}
return descriptionTransport.transport.getCredentials();
}
public IceUdpTransportInfo.Setup getDtlsSetup() { public IceUdpTransportInfo.Setup getDtlsSetup() {
final Set<IceUdpTransportInfo.Setup> setups = ImmutableSet.copyOf(Collections2.transform( final Set<IceUdpTransportInfo.Setup> setups =
contents.values(), ImmutableSet.copyOf(
dt -> dt.transport.getFingerprint().getSetup() Collections2.transform(
)); contents.values(), dt -> dt.transport.getFingerprint().getSetup()));
final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null); final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null);
if (setups.size() == 1 && setup != null) { if (setups.size() == 1 && setup != null) {
return setup; return setup;
@ -185,13 +228,18 @@ public class RtpContentMap {
return count == 0; return count == 0;
} }
public RtpContentMap modifiedCredentials(IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) { public RtpContentMap modifiedCredentials(
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder = new ImmutableMap.Builder<>(); IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
new ImmutableMap.Builder<>();
for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) { for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
final RtpDescription rtpDescription = content.getValue().description; final RtpDescription rtpDescription = content.getValue().description;
IceUdpTransportInfo transportInfo = content.getValue().transport; IceUdpTransportInfo transportInfo = content.getValue().transport;
final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials, setup); final IceUdpTransportInfo modifiedTransportInfo =
contentMapBuilder.put(content.getKey(), new DescriptionTransport(rtpDescription, modifiedTransportInfo)); transportInfo.modifyCredentials(credentials, setup);
contentMapBuilder.put(
content.getKey(),
new DescriptionTransport(rtpDescription, modifiedTransportInfo));
} }
return new RtpContentMap(this.group, contentMapBuilder.build()); return new RtpContentMap(this.group, contentMapBuilder.build());
} }
@ -200,7 +248,8 @@ public class RtpContentMap {
public final RtpDescription description; public final RtpDescription description;
public final IceUdpTransportInfo transport; public final IceUdpTransportInfo transport;
public DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) { public DescriptionTransport(
final RtpDescription description, final IceUdpTransportInfo transport) {
this.description = description; this.description = description;
this.transport = transport; this.transport = transport;
} }
@ -215,27 +264,32 @@ public class RtpContentMap {
} else if (description instanceof RtpDescription) { } else if (description instanceof RtpDescription) {
rtpDescription = (RtpDescription) description; rtpDescription = (RtpDescription) description;
} else { } else {
throw new UnsupportedApplicationException("Content does not contain rtp description"); throw new UnsupportedApplicationException(
"Content does not contain rtp description");
} }
if (transportInfo instanceof IceUdpTransportInfo) { if (transportInfo instanceof IceUdpTransportInfo) {
iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo; iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
} else { } else {
throw new UnsupportedTransportException("Content does not contain ICE-UDP transport"); throw new UnsupportedTransportException(
"Content does not contain ICE-UDP transport");
} }
return new DescriptionTransport( return new DescriptionTransport(
rtpDescription, rtpDescription, OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo)
);
} }
public static DescriptionTransport of(final SessionDescription sessionDescription, final SessionDescription.Media media) { public static DescriptionTransport of(
final SessionDescription sessionDescription, final SessionDescription.Media media) {
final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media); final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
final IceUdpTransportInfo transportInfo = IceUdpTransportInfo.of(sessionDescription, media); final IceUdpTransportInfo transportInfo =
IceUdpTransportInfo.of(sessionDescription, media);
return new DescriptionTransport(rtpDescription, transportInfo); return new DescriptionTransport(rtpDescription, transportInfo);
} }
public static Map<String, DescriptionTransport> of(final Map<String, Content> contents) { public static Map<String, DescriptionTransport> of(final Map<String, Content> contents) {
return ImmutableMap.copyOf(Maps.transformValues(contents, new Function<Content, DescriptionTransport>() { return ImmutableMap.copyOf(
Maps.transformValues(
contents,
new Function<Content, DescriptionTransport>() {
@NullableDecl @NullableDecl
@Override @Override
public DescriptionTransport apply(@NullableDecl Content content) { public DescriptionTransport apply(@NullableDecl Content content) {

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.xmpp.jingle.stanzas; package eu.siacs.conversations.xmpp.jingle.stanzas;
import androidx.annotation.NonNull;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.base.Objects; import com.google.common.base.Objects;
@ -123,6 +125,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
} }
@Override @Override
@NonNull
public String toString() { public String toString() {
return MoreObjects.toStringHelper(this) return MoreObjects.toStringHelper(this)
.add("ufrag", ufrag) .add("ufrag", ufrag)