diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp index 9603139734..eeaad186b3 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 #include @@ -22,6 +23,8 @@ #include #include #include +#include +#include #include #include @@ -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 @@ int initTracker(const std::string& iface, const std::string& pfx96, const std::s 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 @@ static jint com_android_server_connectivity_ClatCoordinator_maybeStartBpf( 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 @@ static jint com_android_server_connectivity_ClatCoordinator_maybeStartBpf( } } - 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 @@ static void com_android_server_connectivity_ClatCoordinator_maybeStopBpf(JNIEnv* net::clat::maybeStopBpf(tracker); } } + + kill(pid, SIGTERM); + waitpid(pid, nullptr, 0); // Should we block in JNI? } /* @@ -344,13 +474,13 @@ static const JNINativeMethod gMethods[] = { (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 5a5e24a99d..761645792f 100644 --- a/service/src/com/android/server/connectivity/ClatCoordinator.java +++ b/service/src/com/android/server/connectivity/ClatCoordinator.java @@ -170,20 +170,20 @@ public class ClatCoordinator { } /** - * 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 class ClatCoordinator { 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 @@ public class ClatCoordinator { 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 @@ public class ClatCoordinator { 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 @@ public class ClatCoordinator { 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 @@ public class ClatCoordinator { 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 @@ public class ClatCoordinator { // 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 @@ public class ClatCoordinator { 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 @@ public class ClatCoordinator { 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 @@ public class ClatCoordinator { mNat64Prefix = null; mXlatLocalAddress4 = null; mXlatLocalAddress6 = null; + mPid = INVALID_PID; } private static native String native_selectIpv4Address(String v4addr, int prefixlen) @@ -372,9 +398,9 @@ public class ClatCoordinator { 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 0000000000..bdcc21b98e --- /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 */)); + } +}