tethering: DAD Proxy Daemon

DAD proxy daemon responsible for forwarding NS/NA between
tethered iface and upstream iface.

Change-Id: I2e58e10e7fa7dba6a6f63ad03b000549f3afc37e
This commit is contained in:
Tyler Wear
2020-03-13 11:38:38 -07:00
parent 1ed9e74716
commit 90e4063fd2
10 changed files with 818 additions and 25 deletions

View File

@@ -17,18 +17,63 @@
#include <errno.h>
#include <error.h>
#include <jni.h>
#include <linux/filter.h>
#include <nativehelper/JNIHelp.h>
#include <nativehelper/JNIHelpCompat.h>
#include <nativehelper/ScopedUtfChars.h>
#include <net/if.h>
#include <netinet/ether.h>
#include <netinet/ip6.h>
#include <netinet/icmp6.h>
#include <sys/socket.h>
#include <stdio.h>
#define LOG_TAG "TetheringUtils"
#include <android/log.h>
namespace android {
static const uint32_t kIPv6NextHeaderOffset = offsetof(ip6_hdr, ip6_nxt);
static const uint32_t kIPv6PayloadStart = sizeof(ip6_hdr);
static const uint32_t kICMPv6TypeOffset = kIPv6PayloadStart + offsetof(icmp6_hdr, icmp6_type);
static void android_net_util_setupIcmpFilter(JNIEnv *env, jobject javaFd, uint32_t type) {
sock_filter filter_code[] = {
// Check header is ICMPv6.
BPF_STMT(BPF_LD | BPF_B | BPF_ABS, kIPv6NextHeaderOffset),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_ICMPV6, 0, 3),
// Check ICMPv6 type.
BPF_STMT(BPF_LD | BPF_B | BPF_ABS, kICMPv6TypeOffset),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, type, 0, 1),
// Accept or reject.
BPF_STMT(BPF_RET | BPF_K, 0xffff),
BPF_STMT(BPF_RET | BPF_K, 0)
};
const sock_fprog filter = {
sizeof(filter_code) / sizeof(filter_code[0]),
filter_code,
};
int fd = jniGetFDFromFileDescriptor(env, javaFd);
if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) {
jniThrowExceptionFmt(env, "java/net/SocketException",
"setsockopt(SO_ATTACH_FILTER): %s", strerror(errno));
}
}
static void android_net_util_setupNaSocket(JNIEnv *env, jobject clazz, jobject javaFd)
{
android_net_util_setupIcmpFilter(env, javaFd, ND_NEIGHBOR_ADVERT);
}
static void android_net_util_setupNsSocket(JNIEnv *env, jobject clazz, jobject javaFd)
{
android_net_util_setupIcmpFilter(env, javaFd, ND_NEIGHBOR_SOLICIT);
}
static void android_net_util_setupRaSocket(JNIEnv *env, jobject clazz, jobject javaFd,
jint ifIndex)
{
@@ -125,7 +170,12 @@ static void android_net_util_setupRaSocket(JNIEnv *env, jobject clazz, jobject j
*/
static const JNINativeMethod gMethods[] = {
/* name, signature, funcPtr */
{ "setupRaSocket", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_util_setupRaSocket },
{ "setupNaSocket", "(Ljava/io/FileDescriptor;)V",
(void*) android_net_util_setupNaSocket },
{ "setupNsSocket", "(Ljava/io/FileDescriptor;)V",
(void*) android_net_util_setupNsSocket },
{ "setupRaSocket", "(Ljava/io/FileDescriptor;I)V",
(void*) android_net_util_setupRaSocket },
};
int register_android_net_util_TetheringUtils(JNIEnv* env) {

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 android.net.ip;
import android.net.util.InterfaceParams;
import android.os.Handler;
import androidx.annotation.VisibleForTesting;
/**
* Basic Duplicate address detection proxy.
*
* @hide
*/
public class DadProxy {
private static final String TAG = DadProxy.class.getSimpleName();
@VisibleForTesting
public static NeighborPacketForwarder naForwarder;
public static NeighborPacketForwarder nsForwarder;
public DadProxy(Handler h, InterfaceParams tetheredIface) {
naForwarder = new NeighborPacketForwarder(h, tetheredIface,
NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT);
nsForwarder = new NeighborPacketForwarder(h, tetheredIface,
NeighborPacketForwarder.ICMPV6_NEIGHBOR_SOLICITATION);
}
/** Stop NS/NA Forwarders. */
public void stop() {
naForwarder.stop();
nsForwarder.stop();
}
/** Set upstream iface on both forwarders. */
public void setUpstreamIface(InterfaceParams upstreamIface) {
naForwarder.setUpstreamIface(upstreamIface);
nsForwarder.setUpstreamIface(upstreamIface);
}
}

View File

@@ -51,6 +51,7 @@ import android.net.util.InterfaceParams;
import android.net.util.InterfaceSet;
import android.net.util.PrefixUtils;
import android.net.util.SharedLog;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -160,6 +161,15 @@ public class IpServer extends StateMachine {
/** Capture IpServer dependencies, for injection. */
public abstract static class Dependencies {
/**
* Create a DadProxy instance to be used by IpServer.
* To support multiple tethered interfaces concurrently DAD Proxy
* needs to be supported per IpServer instead of per upstream.
*/
public DadProxy getDadProxy(Handler handler, InterfaceParams ifParams) {
return new DadProxy(handler, ifParams);
}
/** Create an IpNeighborMonitor to be used by this IpServer */
public IpNeighborMonitor getIpNeighborMonitor(Handler handler, SharedLog log,
IpNeighborMonitor.NeighborEventConsumer consumer) {
@@ -256,6 +266,7 @@ public class IpServer extends StateMachine {
// Advertisements (otherwise, we do not add them to mLinkProperties at all).
private LinkProperties mLastIPv6LinkProperties;
private RouterAdvertisementDaemon mRaDaemon;
private DadProxy mDadProxy;
// To be accessed only on the handler thread
private int mDhcpServerStartIndex = 0;
@@ -674,6 +685,13 @@ public class IpServer extends StateMachine {
return false;
}
// TODO: use ShimUtils instead of explicitly checking the version here.
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R || "S".equals(Build.VERSION.CODENAME)
|| "T".equals(Build.VERSION.CODENAME)) {
// DAD Proxy starts forwarding packets after IPv6 upstream is present.
mDadProxy = mDeps.getDadProxy(getHandler(), mInterfaceParams);
}
return true;
}
@@ -685,6 +703,11 @@ public class IpServer extends StateMachine {
mRaDaemon.stop();
mRaDaemon = null;
}
if (mDadProxy != null) {
mDadProxy.stop();
mDadProxy = null;
}
}
// IPv6TetheringCoordinator sends updates with carefully curated IPv6-only
@@ -702,11 +725,16 @@ public class IpServer extends StateMachine {
}
RaParams params = null;
int upstreamIfindex = 0;
String upstreamIface = null;
InterfaceParams upstreamIfaceParams = null;
int upstreamIfIndex = 0;
if (v6only != null) {
final String upstreamIface = v6only.getInterfaceName();
upstreamIface = v6only.getInterfaceName();
upstreamIfaceParams = mDeps.getInterfaceParams(upstreamIface);
if (upstreamIfaceParams != null) {
upstreamIfIndex = upstreamIfaceParams.index;
}
params = new RaParams();
params.mtu = v6only.getMtu();
params.hasDefaultRoute = v6only.hasIpv6DefaultRoute();
@@ -726,15 +754,13 @@ public class IpServer extends StateMachine {
}
}
upstreamIfindex = mDeps.getIfindex(upstreamIface);
// Add upstream index to name mapping for the tether stats usage in the coordinator.
// Although this mapping could be added by both class Tethering and IpServer, adding
// mapping from IpServer guarantees that the mapping is added before the adding
// forwarding rules. That is because there are different state machines in both
// classes. It is hard to guarantee the link property update order between multiple
// state machines.
mBpfCoordinator.addUpstreamNameToLookupTable(upstreamIfindex, upstreamIface);
mBpfCoordinator.addUpstreamNameToLookupTable(upstreamIfIndex, upstreamIface);
}
// If v6only is null, we pass in null to setRaParams(), which handles
@@ -743,8 +769,11 @@ public class IpServer extends StateMachine {
setRaParams(params);
mLastIPv6LinkProperties = v6only;
updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, upstreamIfindex, null);
mLastIPv6UpstreamIfindex = upstreamIfindex;
updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, upstreamIfIndex, null);
mLastIPv6UpstreamIfindex = upstreamIfIndex;
if (mDadProxy != null) {
mDadProxy.setUpstreamIface(upstreamIfaceParams);
}
}
private void removeRoutesFromLocalNetwork(@NonNull final List<RouteInfo> toBeRemoved) {

View File

@@ -0,0 +1,180 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 android.net.ip;
import static android.system.OsConstants.AF_INET6;
import static android.system.OsConstants.AF_PACKET;
import static android.system.OsConstants.ETH_P_IPV6;
import static android.system.OsConstants.IPPROTO_RAW;
import static android.system.OsConstants.SOCK_DGRAM;
import static android.system.OsConstants.SOCK_NONBLOCK;
import static android.system.OsConstants.SOCK_RAW;
import android.net.util.InterfaceParams;
import android.net.util.PacketReader;
import android.net.util.SocketUtils;
import android.net.util.TetheringUtils;
import android.os.Handler;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;
import java.io.FileDescriptor;
import java.io.IOException;
import java.net.Inet6Address;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Arrays;
/**
* Basic IPv6 Neighbor Advertisement Forwarder.
*
* Forward NA packets from upstream iface to tethered iface
* and NS packets from tethered iface to upstream iface.
*
* @hide
*/
public class NeighborPacketForwarder extends PacketReader {
private final String mTag;
private FileDescriptor mFd;
// TODO: get these from NetworkStackConstants.
private static final int IPV6_ADDR_LEN = 16;
private static final int IPV6_DST_ADDR_OFFSET = 24;
private static final int IPV6_HEADER_LEN = 40;
private static final int ETH_HEADER_LEN = 14;
private InterfaceParams mListenIfaceParams, mSendIfaceParams;
private final int mType;
public static final int ICMPV6_NEIGHBOR_ADVERTISEMENT = 136;
public static final int ICMPV6_NEIGHBOR_SOLICITATION = 135;
public NeighborPacketForwarder(Handler h, InterfaceParams tetheredInterface, int type) {
super(h);
mTag = NeighborPacketForwarder.class.getSimpleName() + "-"
+ tetheredInterface.name + "-" + type;
mType = type;
if (mType == ICMPV6_NEIGHBOR_ADVERTISEMENT) {
mSendIfaceParams = tetheredInterface;
} else {
mListenIfaceParams = tetheredInterface;
}
}
/** Set new upstream iface and start/stop based on new params. */
public void setUpstreamIface(InterfaceParams upstreamParams) {
final InterfaceParams oldUpstreamParams;
if (mType == ICMPV6_NEIGHBOR_ADVERTISEMENT) {
oldUpstreamParams = mListenIfaceParams;
mListenIfaceParams = upstreamParams;
} else {
oldUpstreamParams = mSendIfaceParams;
mSendIfaceParams = upstreamParams;
}
if (oldUpstreamParams == null && upstreamParams != null) {
start();
} else if (oldUpstreamParams != null && upstreamParams == null) {
stop();
} else if (oldUpstreamParams != null && upstreamParams != null
&& oldUpstreamParams.index != upstreamParams.index) {
stop();
start();
}
}
// TODO: move NetworkStackUtils.closeSocketQuietly to
// frameworks/libs/net/common/device/com/android/net/module/util/[someclass].
private void closeSocketQuietly(FileDescriptor fd) {
try {
SocketUtils.closeSocket(fd);
} catch (IOException ignored) {
}
}
@Override
protected FileDescriptor createFd() {
try {
// ICMPv6 packets from modem do not have eth header, so RAW socket cannot be used.
// To keep uniformity in both directions PACKET socket can be used.
mFd = Os.socket(AF_PACKET, SOCK_DGRAM | SOCK_NONBLOCK, 0);
// TODO: convert setup*Socket to setupICMPv6BpfFilter with filter type?
if (mType == ICMPV6_NEIGHBOR_ADVERTISEMENT) {
TetheringUtils.setupNaSocket(mFd);
} else if (mType == ICMPV6_NEIGHBOR_SOLICITATION) {
TetheringUtils.setupNsSocket(mFd);
}
SocketAddress bindAddress = SocketUtils.makePacketSocketAddress(
ETH_P_IPV6, mListenIfaceParams.index);
Os.bind(mFd, bindAddress);
} catch (ErrnoException | SocketException e) {
Log.wtf(mTag, "Failed to create socket", e);
closeSocketQuietly(mFd);
return null;
}
return mFd;
}
private Inet6Address getIpv6DestinationAddress(byte[] recvbuf) {
Inet6Address dstAddr;
try {
dstAddr = (Inet6Address) Inet6Address.getByAddress(Arrays.copyOfRange(recvbuf,
IPV6_DST_ADDR_OFFSET, IPV6_DST_ADDR_OFFSET + IPV6_ADDR_LEN));
} catch (UnknownHostException | ClassCastException impossible) {
throw new AssertionError("16-byte array not valid IPv6 address?");
}
return dstAddr;
}
@Override
protected void handlePacket(byte[] recvbuf, int length) {
if (mSendIfaceParams == null) {
return;
}
// The BPF filter should already have checked the length of the packet, but...
if (length < IPV6_HEADER_LEN) {
return;
}
Inet6Address destv6 = getIpv6DestinationAddress(recvbuf);
if (!destv6.isMulticastAddress()) {
return;
}
InetSocketAddress dest = new InetSocketAddress(destv6, 0);
FileDescriptor fd = null;
try {
fd = Os.socket(AF_INET6, SOCK_RAW | SOCK_NONBLOCK, IPPROTO_RAW);
SocketUtils.bindSocketToInterface(fd, mSendIfaceParams.name);
int ret = Os.sendto(fd, recvbuf, 0, length, 0, dest);
} catch (ErrnoException | SocketException e) {
Log.e(mTag, "handlePacket error: " + e);
} finally {
closeSocketQuietly(fd);
}
}
}

View File

@@ -18,6 +18,7 @@ package android.net.ip;
import static android.net.util.NetworkConstants.IPV6_MIN_MTU;
import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH;
import static android.net.util.TetheringUtils.getAllNodesForScopeId;
import static android.system.OsConstants.AF_INET6;
import static android.system.OsConstants.IPPROTO_ICMPV6;
import static android.system.OsConstants.SOCK_RAW;
@@ -44,7 +45,6 @@ import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -92,10 +92,6 @@ public class RouterAdvertisementDaemon {
private static final int DAY_IN_SECONDS = 86_400;
private static final byte[] ALL_NODES = new byte[] {
(byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
};
private final InterfaceParams mInterface;
private final InetSocketAddress mAllNodes;
@@ -240,7 +236,6 @@ public class RouterAdvertisementDaemon {
}
}
public RouterAdvertisementDaemon(InterfaceParams ifParams) {
mInterface = ifParams;
mAllNodes = new InetSocketAddress(getAllNodesForScopeId(mInterface.index), 0);
@@ -363,15 +358,6 @@ public class RouterAdvertisementDaemon {
}
}
private static Inet6Address getAllNodesForScopeId(int scopeId) {
try {
return Inet6Address.getByAddress("ff02::1", ALL_NODES, scopeId);
} catch (UnknownHostException uhe) {
Log.wtf(TAG, "Failed to construct ff02::1 InetAddress: " + uhe);
return null;
}
}
private static byte asByte(int value) {
return (byte) value;
}

View File

@@ -17,11 +17,15 @@ package android.net.util;
import android.net.TetherStatsParcel;
import android.net.TetheringRequestParcel;
import android.util.Log;
import androidx.annotation.NonNull;
import java.io.FileDescriptor;
import java.net.Inet6Address;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Objects;
/**
@@ -30,6 +34,24 @@ import java.util.Objects;
* {@hide}
*/
public class TetheringUtils {
public static final byte[] ALL_NODES = new byte[] {
(byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
};
/**
* Configures a socket for receiving and sending ICMPv6 neighbor advertisments.
* @param fd the socket's {@link FileDescriptor}.
*/
public static native void setupNaSocket(FileDescriptor fd)
throws SocketException;
/**
* Configures a socket for receiving and sending ICMPv6 neighbor solicitations.
* @param fd the socket's {@link FileDescriptor}.
*/
public static native void setupNsSocket(FileDescriptor fd)
throws SocketException;
/**
* The object which records offload Tx/Rx forwarded bytes/packets.
* TODO: Replace the inner class ForwardedStats of class OffloadHardwareInterface with
@@ -129,4 +151,15 @@ public class TetheringUtils {
&& request.exemptFromEntitlementCheck == otherRequest.exemptFromEntitlementCheck
&& request.showProvisioningUi == otherRequest.showProvisioningUi;
}
/** Get inet6 address for all nodes given scope ID. */
public static Inet6Address getAllNodesForScopeId(int scopeId) {
try {
return Inet6Address.getByAddress("ff02::1", ALL_NODES, scopeId);
} catch (UnknownHostException uhe) {
Log.wtf("TetheringUtils", "Failed to construct Inet6Address from "
+ Arrays.toString(ALL_NODES) + " and scopedId " + scopeId);
return null;
}
}
}

View File

@@ -14,8 +14,22 @@
// limitations under the License.
//
java_defaults {
name: "TetheringPrivilegedTestsJniDefaults",
jni_libs: [
"libdexmakerjvmtiagent",
"libstaticjvmtiagent",
"libtetherutilsjni",
],
jni_uses_sdk_apis: true,
visibility: ["//visibility:private"],
}
android_test {
name: "TetheringPrivilegedTests",
defaults: [
"TetheringPrivilegedTestsJniDefaults",
],
srcs: [
"src/**/*.java",
"src/**/*.kt",
@@ -23,8 +37,13 @@ android_test {
certificate: "networkstack",
platform_apis: true,
test_suites: [
"general-tests",
"device-tests",
"mts",
],
static_libs: [
"androidx.test.rules",
"net-tests-utils",
"TetheringApiCurrentLib",
],
compile_multilib: "both",
}

View File

@@ -0,0 +1,338 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 android.net.ip;
import static android.system.OsConstants.IPPROTO_ICMPV6;
import static android.system.OsConstants.IPPROTO_TCP;
import static com.android.internal.util.BitUtils.uint16;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import android.app.Instrumentation;
import android.content.Context;
import android.net.INetd;
import android.net.InetAddresses;
import android.net.MacAddress;
import android.net.TestNetworkInterface;
import android.net.TestNetworkManager;
import android.net.util.InterfaceParams;
import android.net.util.IpUtils;
import android.net.util.TetheringUtils;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.system.ErrnoException;
import android.system.Os;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.testutils.TapPacketReader;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
import java.io.FileDescriptor;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicReference;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class DadProxyTest {
private static final int DATA_BUFFER_LEN = 4096;
private static final int PACKET_TIMEOUT_MS = 5_000;
// TODO: make NetworkStackConstants accessible to this test and use the constant from there.
private static final int ETHER_SRC_ADDR_OFFSET = 6;
private DadProxy mProxy;
TestNetworkInterface mUpstreamTestIface, mTetheredTestIface;
private InterfaceParams mUpstreamParams, mTetheredParams;
private HandlerThread mHandlerThread;
private Handler mHandler;
private TapPacketReader mUpstreamPacketReader, mTetheredPacketReader;
private FileDescriptor mUpstreamTapFd, mTetheredTapFd;
private static INetd sNetd;
@BeforeClass
public static void setupOnce() {
System.loadLibrary("tetherutilsjni");
final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
final IBinder netdIBinder =
(IBinder) inst.getContext().getSystemService(Context.NETD_SERVICE);
sNetd = INetd.Stub.asInterface(netdIBinder);
}
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mHandlerThread = new HandlerThread(getClass().getSimpleName());
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
setupTapInterfaces();
// Looper must be prepared here since AndroidJUnitRunner runs tests on separate threads.
if (Looper.myLooper() == null) Looper.prepare();
DadProxy mProxy = setupProxy();
}
@After
public void tearDown() throws Exception {
if (mHandlerThread != null) {
mHandler.post(mUpstreamPacketReader::stop); // Also closes the socket
mHandler.post(mTetheredPacketReader::stop); // Also closes the socket
mUpstreamTapFd = null;
mTetheredTapFd = null;
mHandlerThread.quitSafely();
}
if (mTetheredParams != null) {
sNetd.networkRemoveInterface(INetd.LOCAL_NET_ID, mTetheredParams.name);
}
if (mUpstreamParams != null) {
sNetd.networkRemoveInterface(INetd.LOCAL_NET_ID, mUpstreamParams.name);
}
if (mUpstreamTestIface != null) {
try {
Os.close(mUpstreamTestIface.getFileDescriptor().getFileDescriptor());
} catch (ErrnoException e) { }
}
if (mTetheredTestIface != null) {
try {
Os.close(mTetheredTestIface.getFileDescriptor().getFileDescriptor());
} catch (ErrnoException e) { }
}
}
private TestNetworkInterface setupTapInterface() {
final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
AtomicReference<TestNetworkInterface> iface = new AtomicReference<>();
inst.getUiAutomation().adoptShellPermissionIdentity();
try {
final TestNetworkManager tnm = (TestNetworkManager) inst.getContext().getSystemService(
Context.TEST_NETWORK_SERVICE);
iface.set(tnm.createTapInterface());
} finally {
inst.getUiAutomation().dropShellPermissionIdentity();
}
return iface.get();
}
private void setupTapInterfaces() {
// Create upstream test iface.
mUpstreamTestIface = setupTapInterface();
mUpstreamParams = InterfaceParams.getByName(mUpstreamTestIface.getInterfaceName());
assertNotNull(mUpstreamParams);
mUpstreamTapFd = mUpstreamTestIface.getFileDescriptor().getFileDescriptor();
mUpstreamPacketReader = new TapPacketReader(mHandler, mUpstreamTapFd,
DATA_BUFFER_LEN);
mHandler.post(mUpstreamPacketReader::start);
// Create tethered test iface.
mTetheredTestIface = setupTapInterface();
mTetheredParams = InterfaceParams.getByName(mTetheredTestIface.getInterfaceName());
assertNotNull(mTetheredParams);
mTetheredTapFd = mTetheredTestIface.getFileDescriptor().getFileDescriptor();
mTetheredPacketReader = new TapPacketReader(mHandler, mTetheredTapFd,
DATA_BUFFER_LEN);
mHandler.post(mTetheredPacketReader::start);
}
private static final int IPV6_HEADER_LEN = 40;
private static final int ETH_HEADER_LEN = 14;
private static final int ICMPV6_NA_NS_LEN = 24;
private static final int LL_TARGET_OPTION_LEN = 8;
private static final int ICMPV6_CHECKSUM_OFFSET = 2;
private static final int ETHER_TYPE_IPV6 = 0x86dd;
// TODO: move the IpUtils code to frameworks/lib/net and link it statically.
private static int checksumFold(int sum) {
while (sum > 0xffff) {
sum = (sum >> 16) + (sum & 0xffff);
}
return sum;
}
// TODO: move the IpUtils code to frameworks/lib/net and link it statically.
private static short checksumAdjust(short checksum, short oldWord, short newWord) {
checksum = (short) ~checksum;
int tempSum = checksumFold(uint16(checksum) + uint16(newWord) + 0xffff - uint16(oldWord));
return (short) ~tempSum;
}
// TODO: move the IpUtils code to frameworks/lib/net and link it statically.
private static short icmpv6Checksum(ByteBuffer buf, int ipOffset, int transportOffset,
int transportLen) {
// The ICMPv6 checksum is the same as the TCP checksum, except the pseudo-header uses
// 58 (ICMPv6) instead of 6 (TCP). Calculate the TCP checksum, and then do an incremental
// checksum adjustment for the change in the next header byte.
short checksum = IpUtils.tcpChecksum(buf, ipOffset, transportOffset, transportLen);
return checksumAdjust(checksum, (short) IPPROTO_TCP, (short) IPPROTO_ICMPV6);
}
private static ByteBuffer createDadPacket(int type) {
// Refer to buildArpPacket()
int icmpLen = ICMPV6_NA_NS_LEN
+ (type == NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT
? LL_TARGET_OPTION_LEN : 0);
final ByteBuffer buf = ByteBuffer.allocate(icmpLen + IPV6_HEADER_LEN + ETH_HEADER_LEN);
// Ethernet header.
final MacAddress srcMac = MacAddress.fromString("33:33:ff:66:77:88");
buf.put(srcMac.toByteArray());
final MacAddress dstMac = MacAddress.fromString("01:02:03:04:05:06");
buf.put(dstMac.toByteArray());
buf.putShort((short) ETHER_TYPE_IPV6);
// IPv6 header
byte[] version = {(byte) 0x60, 0x00, 0x00, 0x00};
buf.put(version); // Version
buf.putShort((byte) icmpLen); // Length
buf.put((byte) IPPROTO_ICMPV6); // Next header
buf.put((byte) 0xff); // Hop limit
final byte[] target =
InetAddresses.parseNumericAddress("fe80::1122:3344:5566:7788").getAddress();
final byte[] src;
final byte[] dst;
if (type == NeighborPacketForwarder.ICMPV6_NEIGHBOR_SOLICITATION) {
src = InetAddresses.parseNumericAddress("::").getAddress();
dst = InetAddresses.parseNumericAddress("ff02::1:ff66:7788").getAddress();
} else {
src = target;
dst = TetheringUtils.ALL_NODES;
}
buf.put(src);
buf.put(dst);
// ICMPv6 Header
buf.put((byte) type); // Type
buf.put((byte) 0x00); // Code
buf.putShort((short) 0); // Checksum
buf.putInt(0); // Reserved
buf.put(target);
if (type == NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT) {
//NA packet has LL target address
//ICMPv6 Option
buf.put((byte) 0x02); // Type
buf.put((byte) 0x01); // Length
byte[] ll_target = MacAddress.fromString("01:02:03:04:05:06").toByteArray();
buf.put(ll_target);
}
// Populate checksum field
final int transportOffset = ETH_HEADER_LEN + IPV6_HEADER_LEN;
final short checksum = icmpv6Checksum(buf, ETH_HEADER_LEN, transportOffset, icmpLen);
buf.putShort(transportOffset + ICMPV6_CHECKSUM_OFFSET, checksum);
buf.flip();
return buf;
}
private DadProxy setupProxy() throws Exception {
DadProxy proxy = new DadProxy(mHandler, mTetheredParams);
mHandler.post(() -> proxy.setUpstreamIface(mUpstreamParams));
// Upstream iface is added to local network to simplify test case.
// Otherwise the test needs to create and destroy a network for the upstream iface.
sNetd.networkAddInterface(INetd.LOCAL_NET_ID, mUpstreamParams.name);
sNetd.networkAddInterface(INetd.LOCAL_NET_ID, mTetheredParams.name);
return proxy;
}
// TODO: change to assert.
private boolean waitForPacket(ByteBuffer packet, TapPacketReader reader) {
byte[] p;
while ((p = reader.popPacket(PACKET_TIMEOUT_MS)) != null) {
final ByteBuffer buffer = ByteBuffer.wrap(p);
if (buffer.compareTo(packet) == 0) return true;
}
return false;
}
private void updateDstMac(ByteBuffer buf, MacAddress mac) {
buf.put(mac.toByteArray());
buf.rewind();
}
private void updateSrcMac(ByteBuffer buf, InterfaceParams ifaceParams) {
buf.position(ETHER_SRC_ADDR_OFFSET);
buf.put(ifaceParams.macAddr.toByteArray());
buf.rewind();
}
@Test
public void testNaForwardingFromUpstreamToTether() throws Exception {
ByteBuffer na = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT);
mUpstreamPacketReader.sendResponse(na);
updateDstMac(na, MacAddress.fromString("33:33:00:00:00:01"));
updateSrcMac(na, mTetheredParams);
assertTrue(waitForPacket(na, mTetheredPacketReader));
}
@Test
// TODO: remove test once DAD works in both directions.
public void testNaForwardingFromTetherToUpstream() throws Exception {
ByteBuffer na = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT);
mTetheredPacketReader.sendResponse(na);
updateDstMac(na, MacAddress.fromString("33:33:00:00:00:01"));
updateSrcMac(na, mTetheredParams);
assertFalse(waitForPacket(na, mUpstreamPacketReader));
}
@Test
public void testNsForwardingFromTetherToUpstream() throws Exception {
ByteBuffer ns = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_SOLICITATION);
mTetheredPacketReader.sendResponse(ns);
updateSrcMac(ns, mUpstreamParams);
assertTrue(waitForPacket(ns, mUpstreamPacketReader));
}
@Test
// TODO: remove test once DAD works in both directions.
public void testNsForwardingFromUpstreamToTether() throws Exception {
ByteBuffer ns = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_SOLICITATION);
mUpstreamPacketReader.sendResponse(ns);
updateSrcMac(ns, mUpstreamParams);
assertFalse(waitForPacket(ns, mTetheredPacketReader));
}
}

View File

@@ -86,6 +86,7 @@ import android.net.util.InterfaceParams;
import android.net.util.InterfaceSet;
import android.net.util.PrefixUtils;
import android.net.util.SharedLog;
import android.os.Build;
import android.os.Handler;
import android.os.RemoteException;
import android.os.test.TestLooper;
@@ -100,8 +101,12 @@ import com.android.networkstack.tethering.BpfCoordinator;
import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
import com.android.networkstack.tethering.PrivateAddressCoordinator;
import com.android.networkstack.tethering.TetheringConfiguration;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -120,6 +125,9 @@ import java.util.List;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class IpServerTest {
@Rule
public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
private static final String IFACE_NAME = "testnet1";
private static final String UPSTREAM_IFACE = "upstream0";
private static final String UPSTREAM_IFACE2 = "upstream1";
@@ -132,6 +140,11 @@ public class IpServerTest {
private static final InterfaceParams TEST_IFACE_PARAMS = new InterfaceParams(
IFACE_NAME, 42 /* index */, MacAddress.ALL_ZEROS_ADDRESS, 1500 /* defaultMtu */);
private static final InterfaceParams UPSTREAM_IFACE_PARAMS = new InterfaceParams(
UPSTREAM_IFACE, UPSTREAM_IFINDEX, MacAddress.ALL_ZEROS_ADDRESS, 1500 /* defaultMtu */);
private static final InterfaceParams UPSTREAM_IFACE_PARAMS2 = new InterfaceParams(
UPSTREAM_IFACE2, UPSTREAM_IFINDEX2, MacAddress.ALL_ZEROS_ADDRESS,
1500 /* defaultMtu */);
private static final int MAKE_DHCPSERVER_TIMEOUT_MS = 1000;
@@ -142,6 +155,7 @@ public class IpServerTest {
@Mock private IpServer.Callback mCallback;
@Mock private SharedLog mSharedLog;
@Mock private IDhcpServer mDhcpServer;
@Mock private DadProxy mDadProxy;
@Mock private RouterAdvertisementDaemon mRaDaemon;
@Mock private IpNeighborMonitor mIpNeighborMonitor;
@Mock private IpServer.Dependencies mDependencies;
@@ -165,8 +179,11 @@ public class IpServerTest {
private void initStateMachine(int interfaceType, boolean usingLegacyDhcp,
boolean usingBpfOffload) throws Exception {
when(mDependencies.getDadProxy(any(), any())).thenReturn(mDadProxy);
when(mDependencies.getRouterAdvertisementDaemon(any())).thenReturn(mRaDaemon);
when(mDependencies.getInterfaceParams(IFACE_NAME)).thenReturn(TEST_IFACE_PARAMS);
when(mDependencies.getInterfaceParams(UPSTREAM_IFACE)).thenReturn(UPSTREAM_IFACE_PARAMS);
when(mDependencies.getInterfaceParams(UPSTREAM_IFACE2)).thenReturn(UPSTREAM_IFACE_PARAMS2);
when(mDependencies.getIfindex(eq(UPSTREAM_IFACE))).thenReturn(UPSTREAM_IFINDEX);
when(mDependencies.getIfindex(eq(UPSTREAM_IFACE2))).thenReturn(UPSTREAM_IFINDEX2);
@@ -1103,4 +1120,78 @@ public class IpServerTest {
}
return true;
}
@Test @IgnoreUpTo(Build.VERSION_CODES.R)
public void dadProxyUpdates() throws Exception {
InOrder inOrder = inOrder(mDadProxy);
initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS);
// Add an upstream without IPv6.
dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0);
inOrder.verify(mDadProxy).setUpstreamIface(null);
// Add IPv6 to the upstream.
LinkProperties lp = new LinkProperties();
lp.setInterfaceName(UPSTREAM_IFACE);
dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, 0);
inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS);
// Change upstream.
// New linkproperties is needed, otherwise changing the iface has no impact.
LinkProperties lp2 = new LinkProperties();
lp2.setInterfaceName(UPSTREAM_IFACE2);
dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp2, 0);
inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS2);
// Lose IPv6 on the upstream...
dispatchTetherConnectionChanged(UPSTREAM_IFACE2, null, 0);
inOrder.verify(mDadProxy).setUpstreamIface(null);
// ... and regain it on a different upstream.
dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, 0);
inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS);
// Lose upstream.
dispatchTetherConnectionChanged(null, null, 0);
inOrder.verify(mDadProxy).setUpstreamIface(null);
// Regain upstream.
dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, 0);
inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS);
// Stop tethering.
mIpServer.stop();
mLooper.dispatchAll();
}
private void checkDadProxyEnabled(boolean expectEnabled) throws Exception {
initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
InOrder inOrder = inOrder(mDadProxy);
// Add IPv6 to the upstream.
LinkProperties lp = new LinkProperties();
lp.setInterfaceName(UPSTREAM_IFACE);
if (expectEnabled) {
inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS);
} else {
inOrder.verifyNoMoreInteractions();
}
// Stop tethering.
mIpServer.stop();
mLooper.dispatchAll();
if (expectEnabled) {
inOrder.verify(mDadProxy).stop();
}
else {
verify(mDependencies, never()).getDadProxy(any(), any());
}
}
@Test @IgnoreAfter(Build.VERSION_CODES.R)
public void testDadProxyUpdates_DisabledUpToR() throws Exception {
checkDadProxyEnabled(false);
}
@Test @IgnoreUpTo(Build.VERSION_CODES.R)
public void testDadProxyUpdates_EnabledAfterR() throws Exception {
checkDadProxyEnabled(true);
}
}

View File

@@ -110,6 +110,7 @@ import android.net.TetheringRequestParcel;
import android.net.dhcp.DhcpServerCallbacks;
import android.net.dhcp.DhcpServingParamsParcel;
import android.net.dhcp.IDhcpServer;
import android.net.ip.DadProxy;
import android.net.ip.IpNeighborMonitor;
import android.net.ip.IpServer;
import android.net.ip.RouterAdvertisementDaemon;
@@ -196,6 +197,7 @@ public class TetheringTest {
@Mock private CarrierConfigManager mCarrierConfigManager;
@Mock private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
@Mock private IPv6TetheringCoordinator mIPv6TetheringCoordinator;
@Mock private DadProxy mDadProxy;
@Mock private RouterAdvertisementDaemon mRouterAdvertisementDaemon;
@Mock private IpNeighborMonitor mIpNeighborMonitor;
@Mock private IDhcpServer mDhcpServer;
@@ -279,6 +281,12 @@ public class TetheringTest {
}
public class MockIpServerDependencies extends IpServer.Dependencies {
@Override
public DadProxy getDadProxy(
Handler handler, InterfaceParams ifParams) {
return mDadProxy;
}
@Override
public RouterAdvertisementDaemon getRouterAdvertisementDaemon(
InterfaceParams ifParams) {
@@ -832,6 +840,7 @@ public class TetheringTest {
verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
sendIPv6TetherUpdates(upstreamState);
verify(mDadProxy, never()).setUpstreamIface(notNull());
verify(mRouterAdvertisementDaemon, never()).buildNewRa(any(), notNull());
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
any(), any());
@@ -858,6 +867,8 @@ public class TetheringTest {
verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
sendIPv6TetherUpdates(upstreamState);
// TODO: add interfaceParams to compare in verify.
verify(mDadProxy, times(1)).setUpstreamIface(notNull());
verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull());
verify(mNetd, times(1)).tetherApplyDnsInterfaces();
}
@@ -874,6 +885,7 @@ public class TetheringTest {
any(), any());
sendIPv6TetherUpdates(upstreamState);
verify(mDadProxy, times(1)).setUpstreamIface(notNull());
verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull());
verify(mNetd, times(1)).tetherApplyDnsInterfaces();
}
@@ -891,6 +903,7 @@ public class TetheringTest {
verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
sendIPv6TetherUpdates(upstreamState);
verify(mDadProxy, times(1)).setUpstreamIface(notNull());
verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull());
verify(mNetd, times(1)).tetherApplyDnsInterfaces();
}