Merge changes from topic "preparation_fork_clatd_from_system_server" am: 43f9729e90 am: 74fe555a17 am: bb4afc3167
Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/1934178
Change-Id: I2f8efd82483481827aa33425e71cebf727aecfc6
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
index 9603139..eeaad18 100644
--- a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+#define LOG_TAG "jniClatCoordinator"
#include <arpa/inet.h>
#include <errno.h>
@@ -22,6 +23,8 @@
#include <log/log.h>
#include <nativehelper/JNIHelp.h>
#include <net/if.h>
+#include <spawn.h>
+#include <sys/wait.h>
#include <string>
#include <netjniutils/netjniutils.h>
@@ -33,9 +36,16 @@
// Sync from system/netd/include/netid_client.h
#define MARK_UNSET 0u
+// Sync from system/netd/server/NetdConstants.h
+#define __INT_STRLEN(i) sizeof(#i)
+#define _INT_STRLEN(i) __INT_STRLEN(i)
+#define INT32_STRLEN _INT_STRLEN(INT32_MIN)
+
#define DEVICEPREFIX "v4-"
namespace android {
+static const char* kClatdPath = "/apex/com.android.tethering/bin/for-system/clatd";
+
static void throwIOException(JNIEnv* env, const char* msg, int error) {
jniThrowExceptionFmt(env, "java/io/IOException", "%s: %s", msg, strerror(error));
}
@@ -282,8 +292,7 @@
return 0;
}
-// TODO: fork clatd and rename to .._startClatd.
-static jint com_android_server_connectivity_ClatCoordinator_maybeStartBpf(
+static jint com_android_server_connectivity_ClatCoordinator_startClatd(
JNIEnv* env, jobject clazz, jobject tunJavaFd, jobject readSockJavaFd,
jobject writeSockJavaFd, jstring iface, jstring pfx96, jstring v4, jstring v6) {
ScopedUtfChars ifaceStr(env, iface);
@@ -291,7 +300,121 @@
ScopedUtfChars v4Str(env, v4);
ScopedUtfChars v6Str(env, v6);
- // Start BPF if any
+ int tunFd = netjniutils::GetNativeFileDescriptor(env, tunJavaFd);
+ if (tunFd < 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "Invalid tun file descriptor");
+ return -1;
+ }
+
+ int readSock = netjniutils::GetNativeFileDescriptor(env, readSockJavaFd);
+ if (readSock < 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "Invalid read socket");
+ return -1;
+ }
+
+ int writeSock = netjniutils::GetNativeFileDescriptor(env, writeSockJavaFd);
+ if (writeSock < 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "Invalid write socket");
+ return -1;
+ }
+
+ // 1. create a throwaway socket to reserve a file descriptor number
+ int passedTunFd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+ if (passedTunFd == -1) {
+ throwIOException(env, "socket(ipv6/udp) for tun fd failed", errno);
+ return -1;
+ }
+ int passedSockRead = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+ if (passedSockRead == -1) {
+ throwIOException(env, "socket(ipv6/udp) for read socket failed", errno);
+ return -1;
+ }
+ int passedSockWrite = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+ if (passedSockWrite == -1) {
+ throwIOException(env, "socket(ipv6/udp) for write socket failed", errno);
+ return -1;
+ }
+
+ // these are the FD we'll pass to clatd on the cli, so need it as a string
+ char passedTunFdStr[INT32_STRLEN];
+ char passedSockReadStr[INT32_STRLEN];
+ char passedSockWriteStr[INT32_STRLEN];
+ snprintf(passedTunFdStr, sizeof(passedTunFdStr), "%d", passedTunFd);
+ snprintf(passedSockReadStr, sizeof(passedSockReadStr), "%d", passedSockRead);
+ snprintf(passedSockWriteStr, sizeof(passedSockWriteStr), "%d", passedSockWrite);
+
+ // 2. we're going to use this as argv[0] to clatd to make ps output more useful
+ std::string progname("clatd-");
+ progname += ifaceStr.c_str();
+
+ // clang-format off
+ const char* args[] = {progname.c_str(),
+ "-i", ifaceStr.c_str(),
+ "-p", pfx96Str.c_str(),
+ "-4", v4Str.c_str(),
+ "-6", v6Str.c_str(),
+ "-t", passedTunFdStr,
+ "-r", passedSockReadStr,
+ "-w", passedSockWriteStr,
+ nullptr};
+ // clang-format on
+
+ // 3. register vfork requirement
+ posix_spawnattr_t attr;
+ if (int ret = posix_spawnattr_init(&attr)) {
+ throwIOException(env, "posix_spawnattr_init failed", ret);
+ return -1;
+ }
+
+ // TODO: use android::base::ScopeGuard.
+ if (int ret = posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK)) {
+ posix_spawnattr_destroy(&attr);
+ throwIOException(env, "posix_spawnattr_setflags failed", ret);
+ return -1;
+ }
+
+ // 4. register dup2() action: this is what 'clears' the CLOEXEC flag
+ // on the tun fd that we want the child clatd process to inherit
+ // (this will happen after the vfork, and before the execve)
+ posix_spawn_file_actions_t fa;
+ if (int ret = posix_spawn_file_actions_init(&fa)) {
+ posix_spawnattr_destroy(&attr);
+ throwIOException(env, "posix_spawn_file_actions_init failed", ret);
+ return -1;
+ }
+
+ if (int ret = posix_spawn_file_actions_adddup2(&fa, tunFd, passedTunFd)) {
+ posix_spawnattr_destroy(&attr);
+ posix_spawn_file_actions_destroy(&fa);
+ throwIOException(env, "posix_spawn_file_actions_adddup2 for tun fd failed", ret);
+ return -1;
+ }
+ if (int ret = posix_spawn_file_actions_adddup2(&fa, readSock, passedSockRead)) {
+ posix_spawnattr_destroy(&attr);
+ posix_spawn_file_actions_destroy(&fa);
+ throwIOException(env, "posix_spawn_file_actions_adddup2 for read socket failed", ret);
+ return -1;
+ }
+ if (int ret = posix_spawn_file_actions_adddup2(&fa, writeSock, passedSockWrite)) {
+ posix_spawnattr_destroy(&attr);
+ posix_spawn_file_actions_destroy(&fa);
+ throwIOException(env, "posix_spawn_file_actions_adddup2 for write socket failed", ret);
+ return -1;
+ }
+
+ // 5. actually perform vfork/dup2/execve
+ pid_t pid;
+ if (int ret = posix_spawn(&pid, kClatdPath, &fa, &attr, (char* const*)args, nullptr)) {
+ posix_spawnattr_destroy(&attr);
+ posix_spawn_file_actions_destroy(&fa);
+ throwIOException(env, "posix_spawn failed", ret);
+ return -1;
+ }
+
+ posix_spawnattr_destroy(&attr);
+ posix_spawn_file_actions_destroy(&fa);
+
+ // 5. Start BPF if any
if (!net::clat::initMaps()) {
net::clat::ClatdTracker tracker = {};
if (!initTracker(ifaceStr.c_str(), pfx96Str.c_str(), v4Str.c_str(), v6Str.c_str(),
@@ -300,19 +423,23 @@
}
}
- return 0; // TODO: return forked clatd pid.
+ return pid;
}
-// TODO: stop clatd and rename to .._stopClatd.
-static void com_android_server_connectivity_ClatCoordinator_maybeStopBpf(JNIEnv* env, jobject clazz,
- jstring iface, jstring pfx96,
- jstring v4, jstring v6,
- jint pid /* unused */) {
+static void com_android_server_connectivity_ClatCoordinator_stopClatd(JNIEnv* env, jobject clazz,
+ jstring iface, jstring pfx96,
+ jstring v4, jstring v6,
+ jint pid) {
ScopedUtfChars ifaceStr(env, iface);
ScopedUtfChars pfx96Str(env, pfx96);
ScopedUtfChars v4Str(env, v4);
ScopedUtfChars v6Str(env, v6);
+ if (pid <= 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "Invalid pid");
+ return;
+ }
+
if (!net::clat::initMaps()) {
net::clat::ClatdTracker tracker = {};
if (!initTracker(ifaceStr.c_str(), pfx96Str.c_str(), v4Str.c_str(), v6Str.c_str(),
@@ -320,6 +447,9 @@
net::clat::maybeStopBpf(tracker);
}
}
+
+ kill(pid, SIGTERM);
+ waitpid(pid, nullptr, 0); // Should we block in JNI?
}
/*
@@ -344,13 +474,13 @@
(void*)com_android_server_connectivity_ClatCoordinator_addAnycastSetsockopt},
{"native_configurePacketSocket", "(Ljava/io/FileDescriptor;Ljava/lang/String;I)V",
(void*)com_android_server_connectivity_ClatCoordinator_configurePacketSocket},
- {"native_maybeStartBpf",
+ {"native_startClatd",
"(Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;Ljava/lang/"
"String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I",
- (void*)com_android_server_connectivity_ClatCoordinator_maybeStartBpf},
- {"native_maybeStopBpf",
+ (void*)com_android_server_connectivity_ClatCoordinator_startClatd},
+ {"native_stopClatd",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V",
- (void*)com_android_server_connectivity_ClatCoordinator_maybeStopBpf},
+ (void*)com_android_server_connectivity_ClatCoordinator_stopClatd},
};
int register_android_server_connectivity_ClatCoordinator(JNIEnv* env) {
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index 5a5e24a..7616457 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -170,20 +170,20 @@
}
/**
- * Maybe start bpf.
+ * Start clatd.
*/
- public int maybeStartBpf(@NonNull FileDescriptor tunfd, @NonNull FileDescriptor readsock6,
+ public int startClatd(@NonNull FileDescriptor tunfd, @NonNull FileDescriptor readsock6,
@NonNull FileDescriptor writesock6, @NonNull String iface, @NonNull String pfx96,
@NonNull String v4, @NonNull String v6) throws IOException {
- return native_maybeStartBpf(tunfd, readsock6, writesock6, iface, pfx96, v4, v6);
+ return native_startClatd(tunfd, readsock6, writesock6, iface, pfx96, v4, v6);
}
/**
- * Maybe stop bpf.
+ * Stop clatd.
*/
- public void maybeStopBpf(String iface, String pfx96, String v4, String v6, int pid)
+ public void stopClatd(String iface, String pfx96, String v4, String v6, int pid)
throws IOException {
- native_maybeStopBpf(iface, pfx96, v4, v6, pid);
+ native_stopClatd(iface, pfx96, v4, v6, pid);
}
}
@@ -219,6 +219,9 @@
public String clatStart(final String iface, final int netId,
@NonNull final IpPrefix nat64Prefix)
throws IOException {
+ if (mIface != null || mPid != INVALID_PID) {
+ throw new IOException("Clatd has started on " + mIface + " (pid " + mPid + ")");
+ }
if (nat64Prefix.getPrefixLength() != 96) {
throw new IOException("Prefix must be 96 bits long: " + nat64Prefix);
}
@@ -254,6 +257,7 @@
try {
mNetd.interfaceSetEnableIPv6(tunIface, false /* enabled */);
} catch (RemoteException | ServiceSpecificException e) {
+ tunFd.close();
Log.e(TAG, "Disable IPv6 on " + tunIface + " failed: " + e);
}
@@ -270,6 +274,7 @@
try {
mNetd.interfaceSetMtu(tunIface, mtu);
} catch (RemoteException | ServiceSpecificException e) {
+ tunFd.close();
throw new IOException("Set MTU " + mtu + " on " + tunIface + " failed: " + e);
}
final InterfaceConfigurationParcel ifConfig = new InterfaceConfigurationParcel();
@@ -281,6 +286,7 @@
try {
mNetd.interfaceSetCfg(ifConfig);
} catch (RemoteException | ServiceSpecificException e) {
+ tunFd.close();
throw new IOException("Setting IPv4 address to " + ifConfig.ipv4Addr + "/"
+ ifConfig.prefixLength + " failed on " + ifConfig.ifName + ": " + e);
}
@@ -290,11 +296,12 @@
final ParcelFileDescriptor readSock6;
try {
// Use a JNI call to get native file descriptor instead of Os.socket() because we would
- // like to use ParcelFileDescriptor to close file descriptor automatically. But ctor
+ // like to use ParcelFileDescriptor to manage file descriptor. But ctor
// ParcelFileDescriptor(FileDescriptor fd) is a @hide function. Need to use native file
// descriptor to initialize ParcelFileDescriptor object instead.
readSock6 = mDeps.adoptFd(mDeps.openPacketSocket());
} catch (IOException e) {
+ tunFd.close();
throw new IOException("Open packet socket failed: " + e);
}
@@ -305,11 +312,16 @@
// reason why we use jniOpenPacketSocket6().
writeSock6 = mDeps.adoptFd(mDeps.openRawSocket6(fwmark));
} catch (IOException e) {
+ tunFd.close();
+ readSock6.close();
throw new IOException("Open raw socket failed: " + e);
}
final int ifaceIndex = mDeps.getInterfaceIndex(iface);
if (ifaceIndex == INVALID_IFINDEX) {
+ tunFd.close();
+ readSock6.close();
+ writeSock6.close();
throw new IOException("Fail to get interface index for interface " + iface);
}
@@ -317,6 +329,9 @@
try {
mDeps.addAnycastSetsockopt(writeSock6.getFileDescriptor(), v6, ifaceIndex);
} catch (IOException e) {
+ tunFd.close();
+ readSock6.close();
+ writeSock6.close();
throw new IOException("add anycast sockopt failed: " + e);
}
@@ -324,31 +339,41 @@
try {
mDeps.configurePacketSocket(readSock6.getFileDescriptor(), v6, ifaceIndex);
} catch (IOException e) {
+ tunFd.close();
+ readSock6.close();
+ writeSock6.close();
throw new IOException("configure packet socket failed: " + e);
}
- // [5] Maybe start bpf.
+ // [5] Start clatd.
try {
- mDeps.maybeStartBpf(tunFd.getFileDescriptor(), readSock6.getFileDescriptor(),
+ mPid = mDeps.startClatd(tunFd.getFileDescriptor(), readSock6.getFileDescriptor(),
writeSock6.getFileDescriptor(), iface, pfx96, v4, v6);
mIface = iface;
mNat64Prefix = pfx96;
mXlatLocalAddress4 = v4;
mXlatLocalAddress6 = v6;
} catch (IOException e) {
- throw new IOException("Error start bpf on " + iface + ": " + e);
+ throw new IOException("Error start clatd on " + iface + ": " + e);
+ } finally {
+ tunFd.close();
+ readSock6.close();
+ writeSock6.close();
}
- // TODO: start clatd and returns local xlat464 v6 address.
- return null;
+ return v6;
}
/**
* Stop clatd
*/
public void clatStop() throws IOException {
- mDeps.maybeStopBpf(mIface, mNat64Prefix, mXlatLocalAddress4, mXlatLocalAddress6,
- mPid /* unused */);
+ if (mPid == INVALID_PID) {
+ throw new IOException("Clatd has not started");
+ }
+ Log.i(TAG, "Stopping clatd pid=" + mPid + " on " + mIface);
+
+ mDeps.stopClatd(mIface, mNat64Prefix, mXlatLocalAddress4, mXlatLocalAddress6, mPid);
// TODO: remove setIptablesDropRule
Log.i(TAG, "clatd on " + mIface + " stopped");
@@ -357,6 +382,7 @@
mNat64Prefix = null;
mXlatLocalAddress4 = null;
mXlatLocalAddress6 = null;
+ mPid = INVALID_PID;
}
private static native String native_selectIpv4Address(String v4addr, int prefixlen)
@@ -372,9 +398,9 @@
int ifindex) throws IOException;
private static native void native_configurePacketSocket(FileDescriptor sock, String v6,
int ifindex) throws IOException;
- private static native int native_maybeStartBpf(FileDescriptor tunfd, FileDescriptor readsock6,
+ private static native int native_startClatd(FileDescriptor tunfd, FileDescriptor readsock6,
FileDescriptor writesock6, String iface, String pfx96, String v4, String v6)
throws IOException;
- private static native void native_maybeStopBpf(String iface, String pfx96, String v4,
- String v6, int pid) throws IOException;
+ private static native void native_stopClatd(String iface, String pfx96, String v4, String v6,
+ int pid) throws IOException;
}
diff --git a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
new file mode 100644
index 0000000..bdcc21b
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2022 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 com.android.server.connectivity;
+
+import static android.net.INetd.IF_STATE_UP;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_MTU;
+import static com.android.server.connectivity.ClatCoordinator.CLAT_MAX_MTU;
+import static com.android.server.connectivity.ClatCoordinator.INIT_V4ADDR_PREFIX_LEN;
+import static com.android.server.connectivity.ClatCoordinator.INIT_V4ADDR_STRING;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+
+import android.annotation.NonNull;
+import android.net.INetd;
+import android.net.IpPrefix;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.Objects;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+public class ClatCoordinatorTest {
+ private static final String BASE_IFACE = "test0";
+ private static final String STACKED_IFACE = "v4-test0";
+ private static final int BASE_IFINDEX = 1000;
+
+ private static final IpPrefix NAT64_IP_PREFIX = new IpPrefix("64:ff9b::/96");
+ private static final String NAT64_PREFIX_STRING = "64:ff9b::";
+ private static final int GOOGLE_DNS_4 = 0x08080808; // 8.8.8.8
+ private static final int NETID = 42;
+
+ // The test fwmark means: PERMISSION_SYSTEM (0x2), protectedFromVpn: true,
+ // explicitlySelected: true, netid: 42. For bit field structure definition, see union Fwmark in
+ // system/netd/include/Fwmark.h
+ private static final int MARK = 0xb002a;
+
+ private static final String XLAT_LOCAL_IPV4ADDR_STRING = "192.0.0.46";
+ private static final String XLAT_LOCAL_IPV6ADDR_STRING = "2001:db8:0:b11::464";
+ private static final int CLATD_PID = 10483;
+
+ private static final int TUN_FD = 534;
+ private static final int RAW_SOCK_FD = 535;
+ private static final int PACKET_SOCK_FD = 536;
+ private static final ParcelFileDescriptor TUN_PFD = new ParcelFileDescriptor(
+ new FileDescriptor());
+ private static final ParcelFileDescriptor RAW_SOCK_PFD = new ParcelFileDescriptor(
+ new FileDescriptor());
+ private static final ParcelFileDescriptor PACKET_SOCK_PFD = new ParcelFileDescriptor(
+ new FileDescriptor());
+
+ @Mock private INetd mNetd;
+ @Spy private TestDependencies mDeps = new TestDependencies();
+
+ /**
+ * The dependency injection class is used to mock the JNI functions and system functions
+ * for clatd coordinator control plane. Note that any testing used JNI functions need to
+ * be overridden to avoid calling native methods.
+ */
+ protected class TestDependencies extends ClatCoordinator.Dependencies {
+ /**
+ * Get netd.
+ */
+ @Override
+ public INetd getNetd() {
+ return mNetd;
+ }
+
+ /**
+ * @see ParcelFileDescriptor#adoptFd(int).
+ */
+ @Override
+ public ParcelFileDescriptor adoptFd(int fd) {
+ switch (fd) {
+ case TUN_FD:
+ return TUN_PFD;
+ case RAW_SOCK_FD:
+ return RAW_SOCK_PFD;
+ case PACKET_SOCK_FD:
+ return PACKET_SOCK_PFD;
+ default:
+ fail("unsupported arg: " + fd);
+ return null;
+ }
+ }
+
+ /**
+ * Get interface index for a given interface.
+ */
+ @Override
+ public int getInterfaceIndex(String ifName) {
+ if (BASE_IFACE.equals(ifName)) {
+ return BASE_IFINDEX;
+ }
+ fail("unsupported arg: " + ifName);
+ return -1;
+ }
+
+ /**
+ * Create tun interface for a given interface name.
+ */
+ @Override
+ public int createTunInterface(@NonNull String tuniface) throws IOException {
+ if (STACKED_IFACE.equals(tuniface)) {
+ return TUN_FD;
+ }
+ fail("unsupported arg: " + tuniface);
+ return -1;
+ }
+
+ /**
+ * Pick an IPv4 address for clat.
+ */
+ @Override
+ public String selectIpv4Address(@NonNull String v4addr, int prefixlen)
+ throws IOException {
+ if (INIT_V4ADDR_STRING.equals(v4addr) && INIT_V4ADDR_PREFIX_LEN == prefixlen) {
+ return XLAT_LOCAL_IPV4ADDR_STRING;
+ }
+ fail("unsupported args: " + v4addr + ", " + prefixlen);
+ return null;
+ }
+
+ /**
+ * Generate a checksum-neutral IID.
+ */
+ @Override
+ public String generateIpv6Address(@NonNull String iface, @NonNull String v4,
+ @NonNull String prefix64) throws IOException {
+ if (BASE_IFACE.equals(iface) && XLAT_LOCAL_IPV4ADDR_STRING.equals(v4)
+ && NAT64_PREFIX_STRING.equals(prefix64)) {
+ return XLAT_LOCAL_IPV6ADDR_STRING;
+ }
+ fail("unsupported args: " + iface + ", " + v4 + ", " + prefix64);
+ return null;
+ }
+
+ /**
+ * Detect MTU.
+ */
+ @Override
+ public int detectMtu(@NonNull String platSubnet, int platSuffix, int mark)
+ throws IOException {
+ if (NAT64_PREFIX_STRING.equals(platSubnet) && GOOGLE_DNS_4 == platSuffix
+ && MARK == mark) {
+ return ETHER_MTU;
+ }
+ fail("unsupported args: " + platSubnet + ", " + platSuffix + ", " + mark);
+ return -1;
+ }
+
+ /**
+ * Open IPv6 raw socket and set SO_MARK.
+ */
+ @Override
+ public int openRawSocket6(int mark) throws IOException {
+ if (mark == MARK) {
+ return RAW_SOCK_FD;
+ }
+ fail("unsupported arg: " + mark);
+ return -1;
+ }
+
+ /**
+ * Open packet socket.
+ */
+ @Override
+ public int openPacketSocket() throws IOException {
+ // assume that open socket always successfully because there is no argument to check.
+ return PACKET_SOCK_FD;
+ }
+
+ /**
+ * Add anycast setsockopt.
+ */
+ @Override
+ public void addAnycastSetsockopt(@NonNull FileDescriptor sock, String v6, int ifindex)
+ throws IOException {
+ if (Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), sock)
+ && XLAT_LOCAL_IPV6ADDR_STRING.equals(v6)
+ && BASE_IFINDEX == ifindex) return;
+ fail("unsupported args: " + sock + ", " + v6 + ", " + ifindex);
+ }
+
+ /**
+ * Configure packet socket.
+ */
+ @Override
+ public void configurePacketSocket(@NonNull FileDescriptor sock, String v6, int ifindex)
+ throws IOException {
+ if (Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), sock)
+ && XLAT_LOCAL_IPV6ADDR_STRING.equals(v6)
+ && BASE_IFINDEX == ifindex) return;
+ fail("unsupported args: " + sock + ", " + v6 + ", " + ifindex);
+ }
+
+ /**
+ * Start clatd.
+ */
+ @Override
+ public int startClatd(@NonNull FileDescriptor tunfd, @NonNull FileDescriptor readsock6,
+ @NonNull FileDescriptor writesock6, @NonNull String iface, @NonNull String pfx96,
+ @NonNull String v4, @NonNull String v6) throws IOException {
+ if (Objects.equals(TUN_PFD.getFileDescriptor(), tunfd)
+ && Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), readsock6)
+ && Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), writesock6)
+ && BASE_IFACE.equals(iface)
+ && NAT64_PREFIX_STRING.equals(pfx96)
+ && XLAT_LOCAL_IPV4ADDR_STRING.equals(v4)
+ && XLAT_LOCAL_IPV6ADDR_STRING.equals(v6)) {
+ return CLATD_PID;
+ }
+ fail("unsupported args: " + tunfd + ", " + readsock6 + ", " + writesock6 + ", "
+ + ", " + iface + ", " + v4 + ", " + v6);
+ return -1;
+ }
+
+ /**
+ * Stop clatd.
+ */
+ public void stopClatd(@NonNull String iface, @NonNull String pfx96, @NonNull String v4,
+ @NonNull String v6, int pid) throws IOException {
+ if (pid == -1) {
+ fail("unsupported arg: " + pid);
+ }
+ }
+ };
+
+ @NonNull
+ private ClatCoordinator makeClatCoordinator() throws Exception {
+ final ClatCoordinator coordinator = new ClatCoordinator(mDeps);
+ return coordinator;
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ private boolean assertContainsFlag(String[] flags, String match) {
+ for (String flag : flags) {
+ if (flag.equals(match)) return true;
+ }
+ fail("Missing flag: " + match);
+ return false;
+ }
+
+ @Test
+ public void testStartStopClatd() throws Exception {
+ final ClatCoordinator coordinator = makeClatCoordinator();
+ final InOrder inOrder = inOrder(mNetd, mDeps);
+ clearInvocations(mNetd, mDeps);
+
+ // [1] Start clatd.
+ final String addr6For464xlat = coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX);
+ assertEquals(XLAT_LOCAL_IPV6ADDR_STRING, addr6For464xlat);
+
+ // Pick an IPv4 address.
+ inOrder.verify(mDeps).selectIpv4Address(eq(INIT_V4ADDR_STRING),
+ eq(INIT_V4ADDR_PREFIX_LEN));
+
+ // Generate a checksum-neutral IID.
+ inOrder.verify(mDeps).generateIpv6Address(eq(BASE_IFACE),
+ eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(NAT64_PREFIX_STRING));
+
+ // Open, configure and bring up the tun interface.
+ inOrder.verify(mDeps).createTunInterface(eq(STACKED_IFACE));
+ inOrder.verify(mDeps).adoptFd(eq(TUN_FD));
+ inOrder.verify(mNetd).interfaceSetEnableIPv6(eq(STACKED_IFACE), eq(false /* enable */));
+ inOrder.verify(mDeps).detectMtu(eq(NAT64_PREFIX_STRING), eq(GOOGLE_DNS_4), eq(MARK));
+ inOrder.verify(mNetd).interfaceSetMtu(eq(STACKED_IFACE),
+ eq(1472 /* ETHER_MTU(1500) - MTU_DELTA(28) */));
+ inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
+ STACKED_IFACE.equals(cfg.ifName)
+ && XLAT_LOCAL_IPV4ADDR_STRING.equals(cfg.ipv4Addr)
+ && (32 == cfg.prefixLength)
+ && "".equals(cfg.hwAddr)
+ && assertContainsFlag(cfg.flags, IF_STATE_UP)));
+
+ // Open and configure 464xlat read/write sockets.
+ inOrder.verify(mDeps).openPacketSocket();
+ inOrder.verify(mDeps).adoptFd(eq(PACKET_SOCK_FD));
+ inOrder.verify(mDeps).openRawSocket6(eq(MARK));
+ inOrder.verify(mDeps).adoptFd(eq(RAW_SOCK_FD));
+ inOrder.verify(mDeps).getInterfaceIndex(eq(BASE_IFACE));
+ inOrder.verify(mDeps).addAnycastSetsockopt(
+ argThat(fd -> Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), fd)),
+ eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(BASE_IFINDEX));
+ inOrder.verify(mDeps).configurePacketSocket(
+ argThat(fd -> Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), fd)),
+ eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(BASE_IFINDEX));
+
+ // Start clatd.
+ inOrder.verify(mDeps).startClatd(
+ argThat(fd -> Objects.equals(TUN_PFD.getFileDescriptor(), fd)),
+ argThat(fd -> Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), fd)),
+ argThat(fd -> Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), fd)),
+ eq(BASE_IFACE), eq(NAT64_PREFIX_STRING),
+ eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(XLAT_LOCAL_IPV6ADDR_STRING));
+ inOrder.verifyNoMoreInteractions();
+
+ // [2] Start clatd again failed.
+ assertThrows("java.io.IOException: Clatd has started on test0 (pid 10483)",
+ IOException.class,
+ () -> coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX));
+
+ // [3] Expect clatd to stop successfully.
+ coordinator.clatStop();
+ inOrder.verify(mDeps).stopClatd(eq(BASE_IFACE), eq(NAT64_PREFIX_STRING),
+ eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(CLATD_PID));
+ inOrder.verifyNoMoreInteractions();
+
+ // [4] Expect an IO exception while stopping a clatd that doesn't exist.
+ assertThrows("java.io.IOException: Clatd has not started", IOException.class,
+ () -> coordinator.clatStop());
+ inOrder.verify(mDeps, never()).stopClatd(anyString(), anyString(), anyString(),
+ anyString(), anyInt());
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ public void testGetFwmark() throws Exception {
+ assertEquals(0xb0064, ClatCoordinator.getFwmark(100));
+ assertEquals(0xb03e8, ClatCoordinator.getFwmark(1000));
+ assertEquals(0xb2710, ClatCoordinator.getFwmark(10000));
+ assertEquals(0xbffff, ClatCoordinator.getFwmark(65535));
+ }
+
+ @Test
+ public void testAdjustMtu() throws Exception {
+ // Expected mtu is that IPV6_MIN_MTU(1280) minus MTU_DELTA(28).
+ assertEquals(1252, ClatCoordinator.adjustMtu(-1 /* detect mtu failed */));
+ assertEquals(1252, ClatCoordinator.adjustMtu(500));
+ assertEquals(1252, ClatCoordinator.adjustMtu(1000));
+ assertEquals(1252, ClatCoordinator.adjustMtu(1280));
+
+ // Expected mtu is that the detected mtu minus MTU_DELTA(28).
+ assertEquals(1372, ClatCoordinator.adjustMtu(1400));
+ assertEquals(1472, ClatCoordinator.adjustMtu(ETHER_MTU));
+ assertEquals(65508, ClatCoordinator.adjustMtu(CLAT_MAX_MTU));
+
+ // Expected mtu is that CLAT_MAX_MTU(65536) minus MTU_DELTA(28).
+ assertEquals(65508, ClatCoordinator.adjustMtu(CLAT_MAX_MTU + 1 /* over maximum mtu */));
+ }
+}