From 879a88299d016fc36a87ef2492636a40a61519d8 Mon Sep 17 00:00:00 2001 From: cruizba Date: Tue, 6 Jul 2021 21:22:30 +0200 Subject: [PATCH] openvidu-server: Add parameter MEDIA_NODES_PUBLIC_IPS to modify public ip on remote media server candidates --- .../server/config/OpenviduConfig.java | 60 ++++++++- .../KurentoParticipantEndpointConfig.java | 4 + .../kurento/endpoint/MediaEndpoint.java | 66 +++++++++- .../utils/ice/IceCandidateDataParser.java | 120 ++++++++++++++++++ .../server/utils/ice/IceCandidateType.java | 5 + .../src/main/resources/application.properties | 1 + 6 files changed, 249 insertions(+), 7 deletions(-) create mode 100644 openvidu-server/src/main/java/io/openvidu/server/utils/ice/IceCandidateDataParser.java create mode 100644 openvidu-server/src/main/java/io/openvidu/server/utils/ice/IceCandidateType.java diff --git a/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java b/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java index 29fc25a6..5e017af7 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java +++ b/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java @@ -191,6 +191,12 @@ public class OpenviduConfig { private String dotenvPath; + // Media Nodes private IPs and Public IPs + // If defined, they will be configured as public IPs of Kurento or Mediasoup + // Key: Private IP + // Value: Public IP + private Map mediaNodesPublicIps = new HashMap<>(); + // Derived properties public static String finalUrl; @@ -365,6 +371,14 @@ public class OpenviduConfig { this.isTurnadminAvailable = available; } + public boolean areMediaNodesPublicIpsDefined() { + return !this.mediaNodesPublicIps.isEmpty(); + } + + public Map getMediaNodesPublicIpsMap() { + return this.mediaNodesPublicIps; + } + public OpenViduRole[] getRolesFromRecordingNotification() { OpenViduRole[] roles; switch (this.openviduRecordingNotification) { @@ -538,6 +552,9 @@ public class OpenviduConfig { openviduForcedCodec = asEnumValue("OPENVIDU_STREAMS_FORCED_VIDEO_CODEC", VideoCodec.class); openviduAllowTranscoding = asBoolean("OPENVIDU_STREAMS_ALLOW_TRANSCODING"); + // Load Public IPS + mediaNodesPublicIps = loadMediaNodePublicIps("MEDIA_NODES_PUBLIC_IPS"); + kmsUrisList = checkKmsUris(); checkCoturnIp(); @@ -1005,4 +1022,45 @@ public class OpenviduConfig { return null; } -} \ No newline at end of file + private Map loadMediaNodePublicIps(String propertyName) { + String mediaNodesPublicIpsRaw = this.asOptionalString(propertyName); + final Map mediaNodesPublicIps = new HashMap<>(); + + if (mediaNodesPublicIpsRaw == null || mediaNodesPublicIpsRaw.isEmpty()) { + return mediaNodesPublicIps; + } + List mediaNodesPublicIpsList = asJsonStringsArray(propertyName); + for(String ipPairStr: mediaNodesPublicIpsList) { + String[] ipPair = ipPairStr.trim().split(":"); + if (ipPair.length != 2) { + addError(propertyName, "Not valid ip pair in " + propertyName + ": " + ipPairStr); + break; + } + String privateIp = ipPair[0]; + String publicIp = ipPair[1]; + isValidIp(propertyName, privateIp); + isValidIp(propertyName, publicIp); + mediaNodesPublicIps.put(privateIp, publicIp); + } + return mediaNodesPublicIps; + } + + private void isValidIp(String propertyName, String ip) { + if (ip != null && !ip.isEmpty()) { + boolean isIP; + try { + final InetAddress inet = InetAddress.getByName(ip); + isIP = inet instanceof Inet4Address || inet instanceof Inet6Address; + if (isIP) { + ip = inet.getHostAddress(); + } + } catch (final UnknownHostException e) { + isIP = false; + } + if (!isIP) { + addError(propertyName, "Is not a valid IP Address (IPv4 or IPv6): " + ip); + } + } + } + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoParticipantEndpointConfig.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoParticipantEndpointConfig.java index 8353219c..97414f75 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoParticipantEndpointConfig.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoParticipantEndpointConfig.java @@ -17,6 +17,7 @@ package io.openvidu.server.kurento.core; +import io.openvidu.server.config.OpenviduConfig; import org.kurento.client.BaseRtpEndpoint; import org.kurento.client.Endpoint; import org.kurento.client.PlayerEndpoint; @@ -41,6 +42,9 @@ public class KurentoParticipantEndpointConfig { @Autowired protected CallDetailRecord CDR; + @Autowired + protected OpenviduConfig openviduConfig; + public void addEndpointListeners(MediaEndpoint endpoint, String typeOfEndpoint) { // WebRtcEndpoint events diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/MediaEndpoint.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/MediaEndpoint.java index 2ecebfe6..aee10794 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/MediaEndpoint.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/MediaEndpoint.java @@ -17,6 +17,8 @@ package io.openvidu.server.kurento.endpoint; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -28,6 +30,8 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import io.openvidu.server.utils.ice.IceCandidateDataParser; +import io.openvidu.server.utils.ice.IceCandidateType; import org.kurento.client.BaseRtpEndpoint; import org.kurento.client.Continuation; import org.kurento.client.Endpoint; @@ -103,6 +107,7 @@ public abstract class MediaEndpoint { private final List gatheredCandidateList = Collections.synchronizedList(new ArrayList<>()); private LinkedList candidates = new LinkedList(); + public String selectedLocalIceCandidate; public String selectedRemoteIceCandidate; public Queue kmsEvents = new ConcurrentLinkedQueue<>(); @@ -568,15 +573,64 @@ public abstract class MediaEndpoint { webEndpoint.addIceCandidateFoundListener(event -> { final IceCandidate candidate = event.getCandidate(); - gatheredCandidateList.add(candidate); - this.owner.logIceCandidate(new WebrtcDebugEvent(this.owner, this.streamId, WebrtcDebugEventIssuer.server, - this.getWebrtcDebugOperation(), WebrtcDebugEventType.iceCandidate, - gson.toJsonTree(candidate).toString())); - - owner.sendIceCandidate(senderPublicId, endpointName, candidate); + if (this.openviduConfig.areMediaNodesPublicIpsDefined()) { + sendCandidatesWithConfiguredIp(senderPublicId, candidate); + } else { + gatheredCandidateList.add(candidate); + this.owner.logIceCandidate(new WebrtcDebugEvent(this.owner, this.streamId, WebrtcDebugEventIssuer.server, + this.getWebrtcDebugOperation(), WebrtcDebugEventType.iceCandidate, + gson.toJsonTree(candidate).toString())); + owner.sendIceCandidate(senderPublicId, endpointName, candidate); + } }); } + private void sendCandidatesWithConfiguredIp(String senderPublicId, IceCandidate candidate) { + // Get media node private IP + String kurentoPrivateIp = this.owner.getSession().getKms().getIp(); + + // Get Ip to be replaced + String ipToReplace = this.openviduConfig.getMediaNodesPublicIpsMap().get(kurentoPrivateIp); + + // If Ip is configured + if (ipToReplace != null && !ipToReplace.isEmpty()) { + + // Candidate which will have the public IP + IceCandidate candidatePublicIp = new IceCandidate(candidate.getCandidate(), candidate.getSdpMid(), + candidate.getSdpMLineIndex()); + IceCandidateDataParser candidatePublicIpParser = new IceCandidateDataParser(candidatePublicIp); + // Only create host candidates to increase priority + if (candidatePublicIpParser.getType() == IceCandidateType.host) { + candidatePublicIpParser.setIp(ipToReplace); + // Max priority for public IP + candidatePublicIpParser.setMaxPriority(); + candidatePublicIp.setCandidate(candidatePublicIpParser.toString()); + + gatheredCandidateList.add(candidatePublicIp); + this.owner.logIceCandidate(new WebrtcDebugEvent(this.owner, this.streamId, WebrtcDebugEventIssuer.server, + this.getWebrtcDebugOperation(), WebrtcDebugEventType.iceCandidate, + gson.toJsonTree(candidatePublicIp).toString())); + owner.sendIceCandidate(senderPublicId, endpointName, candidatePublicIp); + + // Candidate which will have the private IP exposed of the media node + IceCandidate candidatePrivateIp = new IceCandidate(candidate.getCandidate(), candidate.getSdpMid(), + candidate.getSdpMLineIndex()); + IceCandidateDataParser candidatePrivateIpParser = new IceCandidateDataParser(candidatePrivateIp); + // Min priority for private IP + candidatePrivateIpParser.setMinPriority(); + candidatePrivateIpParser.setIp(kurentoPrivateIp); + candidatePrivateIp.setCandidate(candidatePrivateIpParser.toString()); + + gatheredCandidateList.add(candidatePrivateIp); + this.owner.logIceCandidate(new WebrtcDebugEvent(this.owner, this.streamId, WebrtcDebugEventIssuer.server, + this.getWebrtcDebugOperation(), WebrtcDebugEventType.iceCandidate, + gson.toJsonTree(candidatePrivateIp).toString())); + + owner.sendIceCandidate(senderPublicId, endpointName, candidatePrivateIp); + } + } + } + /** * If supported, it instructs the internal endpoint to start gathering * {@link IceCandidate}s. diff --git a/openvidu-server/src/main/java/io/openvidu/server/utils/ice/IceCandidateDataParser.java b/openvidu-server/src/main/java/io/openvidu/server/utils/ice/IceCandidateDataParser.java new file mode 100644 index 00000000..e91a0bc8 --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/utils/ice/IceCandidateDataParser.java @@ -0,0 +1,120 @@ +package io.openvidu.server.utils.ice; + +import org.kurento.client.IceCandidate; + +import java.security.SecureRandom; +import java.util.Objects; + +/** + * Ice candidate data following rfc5245, section-15.1 (only necessary data for OpenVidu) + */ +public class IceCandidateDataParser { + + /** + * Max priority and Min priority possible defined in rfc5245 15.1 + * ": is a positive integer between 1 and (2**31 - 1)" + * MAX_PRIORITY = (2^24)*126 + (2^8)*65535 + 255 + * MIN_PRIORITY = (2^24)*126 + (2^8)*1 + 255 + */ + private final int MAX_PRIORITY = 2130706431; + private final int MIN_PRIORITY = 511; + + /** + * Full string with the candidate + */ + private String[] candidate; + + public IceCandidateDataParser(IceCandidate iceCandidate) { + this.candidate = iceCandidate.getCandidate().split(" "); + } + + public IceCandidateDataParser(String iceCandidate) { + this.candidate = iceCandidate.split(" "); + } + + /** + * Following rfc5245, section-15.1, the candidate foundation id is the 1 th element + * @return + */ + public void setRandomFoundation() { + String prefix = candidate[0].split(":")[0]; + candidate[0] = prefix + ":" + new SecureRandom().nextInt(Integer.MAX_VALUE - 1); + } + + /** + * Following rfc5245, section-15.1, the priority is the 4th element + * @return The priority of the candidate + */ + public String getPriority() { + return candidate[3]; + } + + /** + * Following rfc5245, section-15.1, the priority to replace is the 4th element + * @param priority + */ + public void setPriority(String priority) { + candidate[3] = priority; + } + + /** + * Following rfc5245, section-15.1, set max priority value + */ + public void setMaxPriority() { + this.setPriority(Long.toString(MAX_PRIORITY)); + } + + /** + * Following rfc5245, section-15.1, set min priority value + */ + public void setMinPriority() { + this.setPriority(Long.toString(MIN_PRIORITY)); + } + + /** + * Following rfc5245, section-15.1, the ip to replace is the 4th element + * @return ip of the candidate + */ + public String getIp() { + return candidate[4]; + } + + /** + * Following rfc5245, section-15.1, the ip to replace is the 4th element + * @param ip New ip for the ICE candidate + */ + public void setIp(String ip) { + this.candidate[4] = ip; + } + + /** + * Following rfc5245, section-15.1, the typ to get is the 7th element + * @return typ of the candidate + */ + public IceCandidateType getType() { + return IceCandidateType.valueOf(candidate[7]); + } + + /** + * Following rfc5245, section-15.1, the typ to replace is the 7th element + * @return typ of the candidate + */ + public void setType(IceCandidateType type) { + candidate[7] = type.toString(); + } + + /** Check if Ice candidate type is of the passed argument + * @param iceCandidateType Ice candidate type + * @return + */ + public boolean isType(IceCandidateType iceCandidateType) { + return Objects.equals(iceCandidateType, this.getType()); + } + + @Override + public String toString() { + return String.join(" ", candidate); + } + + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/utils/ice/IceCandidateType.java b/openvidu-server/src/main/java/io/openvidu/server/utils/ice/IceCandidateType.java new file mode 100644 index 00000000..98f221a8 --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/utils/ice/IceCandidateType.java @@ -0,0 +1,5 @@ +package io.openvidu.server.utils.ice; + +public enum IceCandidateType { + host, srflx, prflx, relay +} diff --git a/openvidu-server/src/main/resources/application.properties b/openvidu-server/src/main/resources/application.properties index 6b19ea72..3e3280db 100644 --- a/openvidu-server/src/main/resources/application.properties +++ b/openvidu-server/src/main/resources/application.properties @@ -52,3 +52,4 @@ COTURN_REDIS_IP=127.0.0.1 COTURN_REDIS_DBNAME=0 COTURN_REDIS_PASSWORD=turn COTURN_REDIS_CONNECT_TIMEOUT=30 +MEDIA_NODES_PUBLIC_IPS=[]