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.
This commit is contained in:
paweldomas 2018-04-03 11:28:31 -05:00 committed by Lyubo Marinov
parent 0456df239f
commit 968b279b37
6 changed files with 565 additions and 13 deletions

View File

@ -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

View File

@ -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)
);
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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 */
}
}
}

View File

@ -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<?string>} 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<?string> = (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 {