From 968b279b376febbd3aae5578fe3c0253182976cf Mon Sep 17 00:00:00 2001 From: paweldomas Date: Tue, 3 Apr 2018 11:28:31 -0500 Subject: [PATCH] feat(android): support NAT64 Adds Nat64InfoModule which resolves IPv6 addresses for IPv4 addresses in IPv6 only network where jitsi-meet deployment does not provide any IPv6 addresses as ICE candidates. --- android/sdk/build.gradle | 2 + .../org/jitsi/meet/sdk/JitsiMeetView.java | 4 +- .../org/jitsi/meet/sdk/net/NAT64AddrInfo.java | 238 ++++++++++++++++++ .../meet/sdk/net/NAT64AddrInfoModule.java | 122 +++++++++ .../jitsi/meet/sdk/net/NAT64AddrInfoTest.java | 150 +++++++++++ .../native/RTCPeerConnection.js | 62 ++++- 6 files changed, 565 insertions(+), 13 deletions(-) create mode 100644 android/sdk/src/main/java/org/jitsi/meet/sdk/net/NAT64AddrInfo.java create mode 100644 android/sdk/src/main/java/org/jitsi/meet/sdk/net/NAT64AddrInfoModule.java create mode 100644 android/sdk/src/test/java/org/jitsi/meet/sdk/net/NAT64AddrInfoTest.java diff --git a/android/sdk/build.gradle b/android/sdk/build.gradle index 7ce356bc3..dbf899a53 100644 --- a/android/sdk/build.gradle +++ b/android/sdk/build.gradle @@ -33,6 +33,8 @@ dependencies { compile project(':react-native-vector-icons') compile project(':react-native-webrtc') compile project(':react-native-calendar-events') + + testCompile 'junit:junit:4.12' } // Build process helpers diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java index 6fec4bed7..6e0aa6efd 100644 --- a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java @@ -21,7 +21,6 @@ import android.app.Application; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -78,7 +77,8 @@ public class JitsiMeetView extends FrameLayout { new ExternalAPIModule(reactContext), new PictureInPictureModule(reactContext), new ProximityModule(reactContext), - new WiFiStatsModule(reactContext) + new WiFiStatsModule(reactContext), + new org.jitsi.meet.sdk.net.NAT64AddrInfoModule(reactContext) ); } diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/net/NAT64AddrInfo.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/net/NAT64AddrInfo.java new file mode 100644 index 000000000..389ab9d4c --- /dev/null +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/net/NAT64AddrInfo.java @@ -0,0 +1,238 @@ +/* + * Copyright @ 2018-present Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jitsi.meet.sdk.net; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * Constructs IPv6 addresses for IPv4 addresses in the NAT64 environment. + * + * NAT64 translates IPv4 to IPv6 addresses by adding "well known" prefix and + * suffix configured by the administrator. Those are figured out by discovering + * both IPv6 and IPv4 addresses of a host and then trying to find a place where + * the IPv4 address fits into the format described here: + * https://tools.ietf.org/html/rfc6052#section-2.2 + */ +public class NAT64AddrInfo { + /** + * Coverts bytes array to upper case HEX string. + * + * @param bytes an array of bytes to be converted + * @return ex. "010AFF" for an array of {1, 10, 255}. + */ + static String bytesToHexString(byte[] bytes) { + StringBuilder hexStr = new StringBuilder(); + + for (byte b : bytes) { + hexStr.append(String.format("%02X", b)); + } + + return hexStr.toString(); + } + + /** + * Tries to discover the NAT64 prefix/suffix based on the IPv4 and IPv6 + * addresses resolved for given {@code host}. + * + * @param host the host for which the code will try to discover IPv4 and + * IPv6 addresses which then will be used to figure out the NAT64 prefix. + * @return {@link NAT64AddrInfo} instance if the NAT64 prefix/suffix was + * successfully discovered or {@code null} if it failed for any reason. + * @throws UnknownHostException thrown by {@link InetAddress#getAllByName}. + */ + public static NAT64AddrInfo discover(String host) + throws UnknownHostException { + InetAddress ipv4 = null; + InetAddress ipv6 = null; + + for(InetAddress addr : InetAddress.getAllByName(host)) { + byte[] bytes = addr.getAddress(); + + if (bytes.length == 4) { + ipv4 = addr; + } else if (bytes.length == 16) { + ipv6 = addr; + } + } + + if (ipv4 != null && ipv6 != null) { + return figureOutNAT64AddrInfo(ipv4.getAddress(), ipv6.getAddress()); + } + + return null; + } + + /** + * Based on IPv4 and IPv6 addresses of the same host, the method will make + * an attempt to figure out what are the NAT64 prefix and suffix. + * + * @param ipv4AddrBytes the IPv4 address of the same host in NAT64 network, + * as returned by {@link InetAddress#getAddress()}. + * @param ipv6AddrBytes the IPv6 address of the same host in NAT64 network, + * as returned by {@link InetAddress#getAddress()}. + * @return {@link NAT64AddrInfo} instance which contains the prefix/suffix + * of the current NAT64 network or {@code null} if the prefix could not be + * found. + */ + static NAT64AddrInfo figureOutNAT64AddrInfo( + byte[] ipv4AddrBytes, + byte[] ipv6AddrBytes) { + String ipv6Str = bytesToHexString(ipv6AddrBytes); + String ipv4Str = bytesToHexString(ipv4AddrBytes); + + // NAT64 address format: + // +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + // |PL| 0-------------32--40--48--56--64--72--80--88--96--104---------| + // +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + // |32| prefix |v4(32) | u | suffix | + // +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + // |40| prefix |v4(24) | u |(8)| suffix | + // +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + // |48| prefix |v4(16) | u | (16) | suffix | + // +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + // |56| prefix |(8)| u | v4(24) | suffix | + // +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + // |64| prefix | u | v4(32) | suffix | + // +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + // |96| prefix | v4(32) | + // +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + int prefixLength = 96; + int suffixLength = 0; + String prefix = null; + String suffix = null; + + if (ipv4Str.equalsIgnoreCase(ipv6Str.substring(prefixLength / 4))) { + prefix = ipv6Str.substring(0, prefixLength / 4); + } else { + // Cut out the 'u' octet + ipv6Str = ipv6Str.substring(0, 16) + ipv6Str.substring(18); + + for (prefixLength = 64, suffixLength = 6; prefixLength >= 32; ) { + if (ipv4Str.equalsIgnoreCase( + ipv6Str.substring( + prefixLength / 4, prefixLength / 4 + 8))) { + prefix = ipv6Str.substring(0, prefixLength / 4); + suffix = ipv6Str.substring(ipv6Str.length() - suffixLength); + break; + } + + prefixLength -= 8; + suffixLength += 2; + } + } + + return prefix != null ? new NAT64AddrInfo(prefix, suffix) : null; + } + + /** + * An overload for {@link #hexStringToIPv6String(StringBuilder)}. + * + * @param hexStr a hex representation of IPv6 address bytes. + * @return an IPv6 address string. + */ + static String hexStringToIPv6String(String hexStr) { + return hexStringToIPv6String(new StringBuilder(hexStr)); + } + + /** + * Converts from HEX representation of IPv6 address bytes into IPv6 address + * string which includes the ':' signs. + * + * @param str a hex representation of IPv6 address bytes. + * @return eg. FE80:CD00:0000:0CDA:1357:0000:212F:749C + */ + static String hexStringToIPv6String(StringBuilder str) { + for (int i = 32 - 4; i > 0; i -= 4) { + str.insert(i, ":"); + } + + return str.toString().toUpperCase(); + } + + /** + * Parses an IPv4 address string and returns it's byte array representation. + * + * @param ipv4Address eg. '192.168.3.23' + * @return byte representation of given IPv4 address string. + * @throws IllegalArgumentException if the address is not in valid format. + */ + static byte[] ipv4AddressStringToBytes(String ipv4Address) { + InetAddress address; + + try { + address = InetAddress.getByName(ipv4Address); + } catch (UnknownHostException e) { + throw new IllegalArgumentException( + "Invalid IP address: " + ipv4Address, e); + } + + byte[] bytes = address.getAddress(); + + if (bytes.length != 4) { + throw new IllegalArgumentException( + "Not an IPv4 address: " + ipv4Address); + } + + return bytes; + } + + /** + * The NAT64 prefix added to construct IPv6 from an IPv4 address. + */ + private final String prefix; + + /** + * The NAT64 suffix (if any) used to construct IPv6 from an IPv4 address. + */ + private final String suffix; + + /** + * Creates new instance of {@link NAT64AddrInfo}. + * + * @param prefix the NAT64 prefix. + * @param suffix the NAT64 suffix. + */ + private NAT64AddrInfo(String prefix, String suffix) { + this.prefix = prefix; + this.suffix = suffix; + } + + /** + * Based on the NAT64 prefix and suffix will create an IPv6 representation + * of the given IPv4 address. + * + * @param ipv4Address eg. '192.34.2.3' + * @return IPv6 address string eg. FE80:CD00:0000:0CDA:1357:0000:212F:749C + * @throws IllegalArgumentException if given string is not a valid IPv4 + * address. + */ + public String getIPv6Address(String ipv4Address) { + byte[] ipv4AddressBytes = ipv4AddressStringToBytes(ipv4Address); + StringBuilder newIPv6Str = new StringBuilder(); + + newIPv6Str.append(prefix); + newIPv6Str.append(bytesToHexString(ipv4AddressBytes)); + + if (suffix != null) { + // Insert the 'u' octet. + newIPv6Str.insert(16, "00"); + newIPv6Str.append(suffix); + } + + return hexStringToIPv6String(newIPv6Str); + } +} diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/net/NAT64AddrInfoModule.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/net/NAT64AddrInfoModule.java new file mode 100644 index 000000000..d9de4125d --- /dev/null +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/net/NAT64AddrInfoModule.java @@ -0,0 +1,122 @@ +/* + * Copyright @ 2018-present Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jitsi.meet.sdk.net; + +import android.util.Log; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; + +import java.net.UnknownHostException; + +/** + * This module exposes the functionality of creating an IPv6 representation + * of IPv4 addresses in NAT64 environment. + * + * See[1] and [2] for more info on what NAT64 is. + * [1]: https://tools.ietf.org/html/rfc6146 + * [2]: https://tools.ietf.org/html/rfc6052 + */ +public class NAT64AddrInfoModule extends ReactContextBaseJavaModule { + /** + * The host for which the module wil try to resolve both IPv4 and IPv6 + * addresses in order to figure out the NAT64 prefix. + */ + private final static String HOST = "nat64.jitsi.net"; + + /** + * How long is the {@link NAT64AddrInfo} instance valid. + */ + private final static long INFO_LIFETIME = 60 * 1000; + + /** + * The name of this module. + */ + private final static String MODULE_NAME = "NAT64AddrInfo"; + + /** + * The {@code Log} tag {@code NAT64AddrInfoModule} is to log messages with. + */ + private final static String TAG = MODULE_NAME; + + /** + * The {@link NAT64AddrInfo} instance which holds NAT64 prefix/suffix. + */ + private NAT64AddrInfo info; + + /** + * When {@link #info} was created. + */ + private long infoTimestamp; + + /** + * Creates new {@link NAT64AddrInfoModule}. + * + * @param reactContext the react context to be used by the new module + * instance. + */ + public NAT64AddrInfoModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + /** + * Tries to obtain IPv6 address for given IPv4 address in NAT64 environment. + * + * @param ipv4Address IPv4 address string. + * @param promise a {@link Promise} which will be resolved either with IPv6 + * address for given IPv4 address or with {@code null} if no + * {@link NAT64AddrInfo} was resolved for the current network. Will be + * rejected if given {@code ipv4Address} is not a valid IPv4 address. + */ + @ReactMethod + public void getIPv6Address(String ipv4Address, final Promise promise) { + // Reset if cached for too long. + if (System.currentTimeMillis() - infoTimestamp > INFO_LIFETIME) { + info = null; + } + + if (info == null) { + String host = HOST; + + try { + info = NAT64AddrInfo.discover(host); + } catch (UnknownHostException e) { + Log.e(TAG, "NAT64AddrInfo.discover: " + host, e); + } + infoTimestamp = System.currentTimeMillis(); + } + + String result; + + try { + result = info == null ? null : info.getIPv6Address(ipv4Address); + } catch (IllegalArgumentException exc) { + Log.e(TAG, "Failed to get IPv6 address for: " + ipv4Address, exc); + + // We don't want to reject. It's not a big deal if there's no IPv6 + // address resolved. + result = null; + } + promise.resolve(result); + } + + @Override + public String getName() { + return MODULE_NAME; + } +} diff --git a/android/sdk/src/test/java/org/jitsi/meet/sdk/net/NAT64AddrInfoTest.java b/android/sdk/src/test/java/org/jitsi/meet/sdk/net/NAT64AddrInfoTest.java new file mode 100644 index 000000000..c01ecafc8 --- /dev/null +++ b/android/sdk/src/test/java/org/jitsi/meet/sdk/net/NAT64AddrInfoTest.java @@ -0,0 +1,150 @@ +/* + * Copyright @ 2017-present Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jitsi.meet.sdk.net; + +import org.junit.Test; + +import java.math.BigInteger; +import java.net.UnknownHostException; + +import static org.junit.Assert.*; + +/** + * Tests for {@link NAT64AddrInfo} class. + */ +public class NAT64AddrInfoTest { + /** + * Test case for the 96 prefix length. + */ + @Test + public void test96Prefix() { + testPrefixSuffix( + "260777000000000400000000", "", "203.0.113.1", "23.17.23.3"); + } + + /** + * Test case for the 64 prefix length. + */ + @Test + public void test64Prefix() { + String prefix = "1FF2A227B3AAF3D2"; + String suffix = "BB87C8"; + + testPrefixSuffix(prefix, suffix, "48.46.87.34", "23.87.145.4"); + } + + /** + * Test case for the 56 prefix length. + */ + @Test + public void test56Prefix() { + String prefix = "1FF2A227B3AAF3"; + String suffix = "A2BB87C8"; + + testPrefixSuffix(prefix, suffix, "34.72.234.255", "1.235.3.65"); + } + + /** + * Test case for the 48 prefix length. + */ + @Test + public void test48Prefix() { + String prefix = "1FF2A227B3AA"; + String suffix = "72A2BB87C8"; + + testPrefixSuffix(prefix, suffix, "97.54.3.23", "77.49.0.33"); + } + + /** + * Test case for the 40 prefix length. + */ + @Test + public void test40Prefix() { + String prefix = "1FF2A227B3"; + String suffix = "D972A2BB87C8"; + + testPrefixSuffix(prefix, suffix, "10.23.56.121", "97.65.32.21"); + } + + /** + * Test case for the 32 prefix length. + */ + @Test + public void test32Prefix() + throws UnknownHostException { + String prefix = "1FF2A227"; + String suffix = "20D972A2BB87C8"; + + testPrefixSuffix(prefix, suffix, "162.63.65.189", "135.222.84.206"); + } + + private static String buildIPv6Addr( + String prefix, String suffix, String ipv4Hex) { + String ipv6Str = prefix + ipv4Hex + suffix; + + if (suffix.length() > 0) { + ipv6Str = new StringBuilder(ipv6Str).insert(16, "00").toString(); + } + + return ipv6Str; + } + + private void testPrefixSuffix( + String prefix, String suffix, String ipv4, String otherIPv4) { + byte[] ipv4Bytes = NAT64AddrInfo.ipv4AddressStringToBytes(ipv4); + String ipv4String = NAT64AddrInfo.bytesToHexString(ipv4Bytes); + String ipv6Str = buildIPv6Addr(prefix, suffix, ipv4String); + + BigInteger ipv6Address = new BigInteger(ipv6Str, 16); + + NAT64AddrInfo nat64AddrInfo + = NAT64AddrInfo.figureOutNAT64AddrInfo( + ipv4Bytes, ipv6Address.toByteArray()); + + assertNotNull("Failed to figure out NAT64 info", nat64AddrInfo); + + String newIPv6 = nat64AddrInfo.getIPv6Address(ipv4); + + assertEquals( + NAT64AddrInfo.hexStringToIPv6String(ipv6Address.toString(16)), + newIPv6); + + byte[] ipv4Addr2 = NAT64AddrInfo.ipv4AddressStringToBytes(otherIPv4); + String ipv4Addr2Hex = NAT64AddrInfo.bytesToHexString(ipv4Addr2); + + newIPv6 = nat64AddrInfo.getIPv6Address(otherIPv4); + + assertEquals( + NAT64AddrInfo.hexStringToIPv6String( + buildIPv6Addr(prefix, suffix, ipv4Addr2Hex)), + newIPv6); + } + + @Test + public void testInvalidIPv4Format() { + testInvalidIPv4Format("256.1.2.3"); + testInvalidIPv4Format("FE80:CD00:0000:0CDA:1357:0000:212F:749C"); + } + + private void testInvalidIPv4Format(String ipv4Str) { + try { + NAT64AddrInfo.ipv4AddressStringToBytes(ipv4Str); + fail("Did not throw IllegalArgumentException"); + } catch (IllegalArgumentException exc) { + /* OK */ + } + } +} diff --git a/react/features/base/lib-jitsi-meet/native/RTCPeerConnection.js b/react/features/base/lib-jitsi-meet/native/RTCPeerConnection.js index 0a001b194..ee6ca263c 100644 --- a/react/features/base/lib-jitsi-meet/native/RTCPeerConnection.js +++ b/react/features/base/lib-jitsi-meet/native/RTCPeerConnection.js @@ -231,8 +231,54 @@ function _setRemoteDescription(sessionDescription) { }); } +// XXX The function _synthesizeIPv6FromIPv4Address is not placed relative to the +// other functions in the file according to alphabetical sorting rule of the +// coding style. But eslint wants constants to be defined before they are used. + /** - * Synthesize IPv6 addresses on iOS in order to support IPv6 NAT64 networks. + * Synthesizes an IPv6 address from a specific IPv4 address. + * + * @param {string} ipv4 - The IPv4 address from which an IPv6 address is to be + * synthesized. + * @returns {Promise} A {@code Promise} which gets resolved with the + * IPv6 address synthesized from the specified {@code ipv4} or a falsy value to + * be treated as inability to synthesize an IPv6 address from the specified + * {@code ipv4}. + */ +const _synthesizeIPv6FromIPv4Address: string => Promise = (function() { + // POSIX.getaddrinfo + const { POSIX } = NativeModules; + + if (POSIX) { + const { getaddrinfo } = POSIX; + + if (typeof getaddrinfo === 'function') { + return ipv4 => + getaddrinfo(/* hostname */ ipv4, /* servname */ undefined) + .then(([ { ai_addr: ipv6 } ]) => ipv6); + } + } + + // NAT64AddrInfo.getIPv6Address + const { NAT64AddrInfo } = NativeModules; + + if (NAT64AddrInfo) { + const { getIPv6Address } = NAT64AddrInfo; + + if (typeof getIPv6Address === 'function') { + return getIPv6Address; + } + } + + // There's no POSIX.getaddrinfo or NAT64AddrInfo.getIPv6Address. + return () => + Promise.reject( + 'The impossible just happened! No POSIX.getaddrinfo or' + + ' NAT64AddrInfo.getIPv6Address!'); +})(); + +/** + * Synthesizes IPv6 addresses on iOS in order to support IPv6 NAT64 networks. * * @param {RTCSessionDescription} sdp - The RTCSessionDescription which * specifies the configuration of the remote end of the connection. @@ -240,12 +286,6 @@ function _setRemoteDescription(sessionDescription) { * @returns {Promise} */ function _synthesizeIPv6Addresses(sdp) { - // The synthesis of IPv6 addresses is implemented on iOS only at the time of - // this writing. - if (!NativeModules.POSIX) { - return Promise.resolve(sdp); - } - return ( new Promise(resolve => resolve(_synthesizeIPv6Addresses0(sdp))) .then(({ ips, lines }) => @@ -272,7 +312,6 @@ function _synthesizeIPv6Addresses0(sessionDescription) { let start = 0; const lines = []; const ips = new Map(); - const { getaddrinfo } = NativeModules.POSIX; do { const end = sdp.indexOf('\r\n', start); @@ -311,9 +350,10 @@ function _synthesizeIPv6Addresses0(sessionDescription) { if (v && typeof v === 'string') { resolve(v); } else { - getaddrinfo(ip, undefined).then( - ([ { ai_addr: value } ]) => { - if (value.indexOf(':') === -1 + _synthesizeIPv6FromIPv4Address(ip).then( + value => { + if (!value + || value.indexOf(':') === -1 || value === ips.get(ip)) { ips.delete(ip); } else {