diff --git a/TEST_MAPPING b/TEST_MAPPING index 6996ad9349..9f2ea35cd5 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -32,6 +32,9 @@ // TODO: move to presubmit when known green. { "name": "bpf_existence_test" + }, + { + "name": "libclat_test" } ], "mainline-presubmit": [ diff --git a/service/Android.bp b/service/Android.bp index 76f9153e06..5ba6a0099e 100644 --- a/service/Android.bp +++ b/service/Android.bp @@ -57,12 +57,18 @@ cc_library_shared { ], srcs: [ "jni/com_android_server_TestNetworkService.cpp", + "jni/com_android_server_connectivity_ClatCoordinator.cpp", "jni/onload.cpp", ], stl: "libc++_static", header_libs: [ "libbase_headers", ], + static_libs: [ + "libclat", + "libip_checksum", + "libnetjniutils", + ], shared_libs: [ "liblog", "libnativehelper", diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp new file mode 100644 index 0000000000..a9d794697a --- /dev/null +++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp @@ -0,0 +1,269 @@ +/* + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "libclat/clatutils.h" +#include "nativehelper/scoped_utf_chars.h" + +// Sync from system/netd/include/netid_client.h +#define MARK_UNSET 0u + +namespace android { +static void throwIOException(JNIEnv* env, const char* msg, int error) { + jniThrowExceptionFmt(env, "java/io/IOException", "%s: %s", msg, strerror(error)); +} + +jstring com_android_server_connectivity_ClatCoordinator_selectIpv4Address(JNIEnv* env, + jobject clazz, + jstring v4addr, + jint prefixlen) { + ScopedUtfChars address(env, v4addr); + in_addr ip; + if (inet_pton(AF_INET, address.c_str(), &ip) != 1) { + throwIOException(env, "invalid address", EINVAL); + return nullptr; + } + + // Pick an IPv4 address. + // TODO: this picks the address based on other addresses that are assigned to interfaces, but + // the address is only actually assigned to an interface once clatd starts up. So we could end + // up with two clatd instances with the same IPv4 address. + // Stop doing this and instead pick a free one from the kV4Addr pool. + in_addr v4 = {net::clat::selectIpv4Address(ip, prefixlen)}; + if (v4.s_addr == INADDR_NONE) { + jniThrowExceptionFmt(env, "java/io/IOException", "No free IPv4 address in %s/%d", + address.c_str(), prefixlen); + return nullptr; + } + + char addrstr[INET_ADDRSTRLEN]; + if (!inet_ntop(AF_INET, (void*)&v4, addrstr, sizeof(addrstr))) { + throwIOException(env, "invalid address", EADDRNOTAVAIL); + return nullptr; + } + return env->NewStringUTF(addrstr); +} + +// Picks a random interface ID that is checksum neutral with the IPv4 address and the NAT64 prefix. +jstring com_android_server_connectivity_ClatCoordinator_generateIpv6Address( + JNIEnv* env, jobject clazz, jstring ifaceStr, jstring v4Str, jstring prefix64Str) { + ScopedUtfChars iface(env, ifaceStr); + ScopedUtfChars addr4(env, v4Str); + ScopedUtfChars prefix64(env, prefix64Str); + + if (iface.c_str() == nullptr) { + jniThrowExceptionFmt(env, "java/io/IOException", "Invalid null interface name"); + return nullptr; + } + + in_addr v4; + if (inet_pton(AF_INET, addr4.c_str(), &v4) != 1) { + jniThrowExceptionFmt(env, "java/io/IOException", "Invalid clat v4 address %s", + addr4.c_str()); + return nullptr; + } + + in6_addr nat64Prefix; + if (inet_pton(AF_INET6, prefix64.c_str(), &nat64Prefix) != 1) { + jniThrowExceptionFmt(env, "java/io/IOException", "Invalid prefix %s", prefix64.c_str()); + return nullptr; + } + + in6_addr v6; + if (net::clat::generateIpv6Address(iface.c_str(), v4, nat64Prefix, &v6)) { + jniThrowExceptionFmt(env, "java/io/IOException", + "Unable to find global source address on %s for %s", iface.c_str(), + prefix64.c_str()); + return nullptr; + } + + char addrstr[INET6_ADDRSTRLEN]; + if (!inet_ntop(AF_INET6, (void*)&v6, addrstr, sizeof(addrstr))) { + throwIOException(env, "invalid address", EADDRNOTAVAIL); + return nullptr; + } + return env->NewStringUTF(addrstr); +} + +static jint com_android_server_connectivity_ClatCoordinator_createTunInterface(JNIEnv* env, + jobject clazz, + jstring tuniface) { + ScopedUtfChars v4interface(env, tuniface); + + // open the tun device in non blocking mode as required by clatd + jint fd = open("/dev/net/tun", O_RDWR | O_NONBLOCK | O_CLOEXEC); + if (fd == -1) { + jniThrowExceptionFmt(env, "java/io/IOException", "open tun device failed (%s)", + strerror(errno)); + return -1; + } + + struct ifreq ifr = { + .ifr_flags = IFF_TUN, + }; + strlcpy(ifr.ifr_name, v4interface.c_str(), sizeof(ifr.ifr_name)); + + if (ioctl(fd, TUNSETIFF, &ifr, sizeof(ifr))) { + close(fd); + jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(TUNSETIFF) failed (%s)", + strerror(errno)); + return -1; + } + + return fd; +} + +static jint com_android_server_connectivity_ClatCoordinator_detectMtu(JNIEnv* env, jobject clazz, + jstring platSubnet, + jint plat_suffix, jint mark) { + ScopedUtfChars platSubnetStr(env, platSubnet); + + in6_addr plat_subnet; + if (inet_pton(AF_INET6, platSubnetStr.c_str(), &plat_subnet) != 1) { + jniThrowExceptionFmt(env, "java/io/IOException", "Invalid plat prefix address %s", + platSubnetStr.c_str()); + return -1; + } + + int ret = net::clat::detect_mtu(&plat_subnet, plat_suffix, mark); + if (ret < 0) { + jniThrowExceptionFmt(env, "java/io/IOException", "detect mtu failed: %s", strerror(-ret)); + return -1; + } + + return ret; +} + +static jint com_android_server_connectivity_ClatCoordinator_openPacketSocket(JNIEnv* env, + jobject clazz) { + // Will eventually be bound to htons(ETH_P_IPV6) protocol, + // but only after appropriate bpf filter is attached. + int sock = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (sock < 0) { + throwIOException(env, "packet socket failed", errno); + return -1; + } + return sock; +} + +static jint com_android_server_connectivity_ClatCoordinator_openRawSocket6(JNIEnv* env, + jobject clazz, + jint mark) { + int sock = socket(AF_INET6, SOCK_RAW | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_RAW); + if (sock < 0) { + throwIOException(env, "raw socket failed", errno); + return -1; + } + + // TODO: check the mark validation + if (mark != MARK_UNSET && setsockopt(sock, SOL_SOCKET, SO_MARK, &mark, sizeof(mark)) < 0) { + throwIOException(env, "could not set mark on raw socket", errno); + close(sock); + return -1; + } + + return sock; +} + +static void com_android_server_connectivity_ClatCoordinator_addAnycastSetsockopt( + JNIEnv* env, jobject clazz, jobject javaFd, jstring addr6, jint ifindex) { + int sock = netjniutils::GetNativeFileDescriptor(env, javaFd); + if (sock < 0) { + jniThrowExceptionFmt(env, "java/io/IOException", "Invalid file descriptor"); + return; + } + + ScopedUtfChars addrStr(env, addr6); + + in6_addr addr; + if (inet_pton(AF_INET6, addrStr.c_str(), &addr) != 1) { + jniThrowExceptionFmt(env, "java/io/IOException", "Invalid IPv6 address %s", + addrStr.c_str()); + return; + } + + struct ipv6_mreq mreq = {addr, ifindex}; + int ret = setsockopt(sock, SOL_IPV6, IPV6_JOIN_ANYCAST, &mreq, sizeof(mreq)); + if (ret) { + jniThrowExceptionFmt(env, "java/io/IOException", "setsockopt IPV6_JOIN_ANYCAST failed: %s", + strerror(errno)); + return; + } +} + +static void com_android_server_connectivity_ClatCoordinator_configurePacketSocket( + JNIEnv* env, jobject clazz, jobject javaFd, jstring addr6, jint ifindex) { + ScopedUtfChars addrStr(env, addr6); + + int sock = netjniutils::GetNativeFileDescriptor(env, javaFd); + if (sock < 0) { + jniThrowExceptionFmt(env, "java/io/IOException", "Invalid file descriptor"); + return; + } + + in6_addr addr; + if (inet_pton(AF_INET6, addrStr.c_str(), &addr) != 1) { + jniThrowExceptionFmt(env, "java/io/IOException", "Invalid IPv6 address %s", + addrStr.c_str()); + return; + } + + int ret = net::clat::configure_packet_socket(sock, &addr, ifindex); + if (ret < 0) { + throwIOException(env, "configure packet socket failed", -ret); + return; + } +} + +/* + * JNI registration. + */ +static const JNINativeMethod gMethods[] = { + /* name, signature, funcPtr */ + {"native_selectIpv4Address", "(Ljava/lang/String;I)Ljava/lang/String;", + (void*)com_android_server_connectivity_ClatCoordinator_selectIpv4Address}, + {"native_generateIpv6Address", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + (void*)com_android_server_connectivity_ClatCoordinator_generateIpv6Address}, + {"native_createTunInterface", "(Ljava/lang/String;)I", + (void*)com_android_server_connectivity_ClatCoordinator_createTunInterface}, + {"native_detectMtu", "(Ljava/lang/String;II)I", + (void*)com_android_server_connectivity_ClatCoordinator_detectMtu}, + {"native_openPacketSocket", "()I", + (void*)com_android_server_connectivity_ClatCoordinator_openPacketSocket}, + {"native_openRawSocket6", "(I)I", + (void*)com_android_server_connectivity_ClatCoordinator_openRawSocket6}, + {"native_addAnycastSetsockopt", "(Ljava/io/FileDescriptor;Ljava/lang/String;I)V", + (void*)com_android_server_connectivity_ClatCoordinator_addAnycastSetsockopt}, + {"native_configurePacketSocket", "(Ljava/io/FileDescriptor;Ljava/lang/String;I)V", + (void*)com_android_server_connectivity_ClatCoordinator_configurePacketSocket}, +}; + +int register_android_server_connectivity_ClatCoordinator(JNIEnv* env) { + return jniRegisterNativeMethods(env, "com/android/server/connectivity/ClatCoordinator", + gMethods, NELEM(gMethods)); +} + +}; // namespace android diff --git a/service/jni/onload.cpp b/service/jni/onload.cpp index 00128794bc..04d9671fad 100644 --- a/service/jni/onload.cpp +++ b/service/jni/onload.cpp @@ -20,6 +20,7 @@ namespace android { int register_android_server_TestNetworkService(JNIEnv* env); +int register_android_server_connectivity_ClatCoordinator(JNIEnv* env); extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { JNIEnv *env; @@ -32,6 +33,10 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { return JNI_ERR; } + if (register_android_server_connectivity_ClatCoordinator(env) < 0) { + return JNI_ERR; + } + return JNI_VERSION_1_6; } diff --git a/service/native/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp new file mode 100644 index 0000000000..85407871f2 --- /dev/null +++ b/service/native/libs/libclat/Android.bp @@ -0,0 +1,49 @@ +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +cc_library_static { + name: "libclat", + defaults: ["netd_defaults"], + srcs: ["clatutils.cpp"], + stl: "libc++_static", + static_libs: ["libip_checksum"], + shared_libs: ["liblog"], + export_include_dirs: ["include"], + min_sdk_version: "30", + apex_available: ["com.android.tethering"], +} + +cc_test { + name: "libclat_test", + defaults: ["netd_defaults"], + test_suites: ["device-tests"], + srcs: [ + "clatutils_test.cpp", + ], + static_libs: [ + "libbase", + "libclat", + "libip_checksum", + "libnetd_test_tun_interface", + ], + shared_libs: [ + "liblog", + "libnetutils", + ], + require_root: true, +} \ No newline at end of file diff --git a/service/native/libs/libclat/clatutils.cpp b/service/native/libs/libclat/clatutils.cpp new file mode 100644 index 0000000000..4a125ba9ff --- /dev/null +++ b/service/native/libs/libclat/clatutils.cpp @@ -0,0 +1,268 @@ +// 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. + +#define LOG_TAG "clatutils" + +#include "libclat/clatutils.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include "checksum.h" +} + +// Sync from external/android-clat/clatd.h +#define MAXMTU 65536 +#define PACKETLEN (MAXMTU + sizeof(struct tun_pi)) + +// Sync from system/netd/include/netid_client.h. +#define MARK_UNSET 0u + +namespace android { +namespace net { +namespace clat { + +bool isIpv4AddressFree(in_addr_t addr) { + int s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (s == -1) { + return 0; + } + + // Attempt to connect to the address. If the connection succeeds and getsockname returns the + // same then the address is already assigned to the system and we can't use it. + struct sockaddr_in sin = { + .sin_family = AF_INET, + .sin_port = htons(53), + .sin_addr = {addr}, + }; + socklen_t len = sizeof(sin); + bool inuse = connect(s, (struct sockaddr*)&sin, sizeof(sin)) == 0 && + getsockname(s, (struct sockaddr*)&sin, &len) == 0 && (size_t)len >= sizeof(sin) && + sin.sin_addr.s_addr == addr; + + close(s); + return !inuse; +} + +// Picks a free IPv4 address, starting from ip and trying all addresses in the prefix in order. +// ip - the IP address from the configuration file +// prefixlen - the length of the prefix from which addresses may be selected. +// returns: the IPv4 address, or INADDR_NONE if no addresses were available +in_addr_t selectIpv4Address(const in_addr ip, int16_t prefixlen) { + return selectIpv4AddressInternal(ip, prefixlen, isIpv4AddressFree); +} + +// Only allow testing to use this function directly. Otherwise call selectIpv4Address(ip, pfxlen) +// which has applied valid isIpv4AddressFree function pointer. +in_addr_t selectIpv4AddressInternal(const in_addr ip, int16_t prefixlen, + isIpv4AddrFreeFn isIpv4AddressFreeFunc) { + // Impossible! Only test allows to apply fn. + if (isIpv4AddressFreeFunc == nullptr) { + return INADDR_NONE; + } + + // Don't accept prefixes that are too large because we scan addresses one by one. + if (prefixlen < 16 || prefixlen > 32) { + return INADDR_NONE; + } + + // All these are in host byte order. + in_addr_t mask = 0xffffffff >> (32 - prefixlen) << (32 - prefixlen); + in_addr_t ipv4 = ntohl(ip.s_addr); + in_addr_t first_ipv4 = ipv4; + in_addr_t prefix = ipv4 & mask; + + // Pick the first IPv4 address in the pool, wrapping around if necessary. + // So, for example, 192.0.0.4 -> 192.0.0.5 -> 192.0.0.6 -> 192.0.0.7 -> 192.0.0.0. + do { + if (isIpv4AddressFreeFunc(htonl(ipv4))) { + return htonl(ipv4); + } + ipv4 = prefix | ((ipv4 + 1) & ~mask); + } while (ipv4 != first_ipv4); + + return INADDR_NONE; +} + +// Alters the bits in the IPv6 address to make them checksum neutral with v4 and nat64Prefix. +void makeChecksumNeutral(in6_addr* v6, const in_addr v4, const in6_addr& nat64Prefix) { + // Fill last 8 bytes of IPv6 address with random bits. + arc4random_buf(&v6->s6_addr[8], 8); + + // Make the IID checksum-neutral. That is, make it so that: + // checksum(Local IPv4 | Remote IPv4) = checksum(Local IPv6 | Remote IPv6) + // in other words (because remote IPv6 = NAT64 prefix | Remote IPv4): + // checksum(Local IPv4) = checksum(Local IPv6 | NAT64 prefix) + // Do this by adjusting the two bytes in the middle of the IID. + + uint16_t middlebytes = (v6->s6_addr[11] << 8) + v6->s6_addr[12]; + + uint32_t c1 = ip_checksum_add(0, &v4, sizeof(v4)); + uint32_t c2 = ip_checksum_add(0, &nat64Prefix, sizeof(nat64Prefix)) + + ip_checksum_add(0, v6, sizeof(*v6)); + + uint16_t delta = ip_checksum_adjust(middlebytes, c1, c2); + v6->s6_addr[11] = delta >> 8; + v6->s6_addr[12] = delta & 0xff; +} + +// Picks a random interface ID that is checksum neutral with the IPv4 address and the NAT64 prefix. +int generateIpv6Address(const char* iface, const in_addr v4, const in6_addr& nat64Prefix, + in6_addr* v6) { + int s = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (s == -1) return -errno; + + if (setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, iface, strlen(iface) + 1) == -1) { + close(s); + return -errno; + } + + sockaddr_in6 sin6 = {.sin6_family = AF_INET6, .sin6_addr = nat64Prefix}; + if (connect(s, reinterpret_cast(&sin6), sizeof(sin6)) == -1) { + close(s); + return -errno; + } + + socklen_t len = sizeof(sin6); + if (getsockname(s, reinterpret_cast(&sin6), &len) == -1) { + close(s); + return -errno; + } + + *v6 = sin6.sin6_addr; + + if (IN6_IS_ADDR_UNSPECIFIED(v6) || IN6_IS_ADDR_LOOPBACK(v6) || IN6_IS_ADDR_LINKLOCAL(v6) || + IN6_IS_ADDR_SITELOCAL(v6) || IN6_IS_ADDR_ULA(v6)) { + close(s); + return -ENETUNREACH; + } + + makeChecksumNeutral(v6, v4, nat64Prefix); + close(s); + + return 0; +} + +int detect_mtu(const struct in6_addr* plat_subnet, uint32_t plat_suffix, uint32_t mark) { + // Create an IPv6 UDP socket. + int s = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (s < 0) { + int ret = errno; + ALOGE("socket(AF_INET6, SOCK_DGRAM, 0) failed: %s", strerror(errno)); + return -ret; + } + + // Socket's mark affects routing decisions (network selection) + if ((mark != MARK_UNSET) && setsockopt(s, SOL_SOCKET, SO_MARK, &mark, sizeof(mark))) { + int ret = errno; + ALOGE("setsockopt(SOL_SOCKET, SO_MARK) failed: %s", strerror(errno)); + close(s); + return -ret; + } + + // Try to connect udp socket to plat_subnet(96 bits):plat_suffix(32 bits) + struct sockaddr_in6 dst = { + .sin6_family = AF_INET6, + .sin6_addr = *plat_subnet, + }; + dst.sin6_addr.s6_addr32[3] = plat_suffix; + if (connect(s, (struct sockaddr*)&dst, sizeof(dst))) { + int ret = errno; + ALOGE("connect() failed: %s", strerror(errno)); + close(s); + return -ret; + } + + // Fetch the socket's IPv6 mtu - this is effectively fetching mtu from routing table + int mtu; + socklen_t sz_mtu = sizeof(mtu); + if (getsockopt(s, SOL_IPV6, IPV6_MTU, &mtu, &sz_mtu)) { + int ret = errno; + ALOGE("getsockopt(SOL_IPV6, IPV6_MTU) failed: %s", strerror(errno)); + close(s); + return -ret; + } + if (sz_mtu != sizeof(mtu)) { + ALOGE("getsockopt(SOL_IPV6, IPV6_MTU) returned unexpected size: %d", sz_mtu); + close(s); + return -EFAULT; + } + close(s); + + return mtu; +} + +/* function: configure_packet_socket + * Binds the packet socket and attaches the receive filter to it. + * sock - the socket to configure + * addr - the IP address to filter + * ifindex - index of interface to add the filter to + * returns: 0 on success, -errno on failure + */ +int configure_packet_socket(int sock, in6_addr* addr, int ifindex) { + uint32_t* ipv6 = addr->s6_addr32; + + // clang-format off + struct sock_filter filter_code[] = { + // Load the first four bytes of the IPv6 destination address (starts 24 bytes in). + // Compare it against the first four bytes of our IPv6 address, in host byte order (BPF loads + // are always in host byte order). If it matches, continue with next instruction (JMP 0). If it + // doesn't match, jump ahead to statement that returns 0 (ignore packet). Repeat for the other + // three words of the IPv6 address, and if they all match, return PACKETLEN (accept packet). + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, 24), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, htonl(ipv6[0]), 0, 7), + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, 28), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, htonl(ipv6[1]), 0, 5), + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, 32), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, htonl(ipv6[2]), 0, 3), + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, 36), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, htonl(ipv6[3]), 0, 1), + BPF_STMT(BPF_RET | BPF_K, PACKETLEN), + BPF_STMT(BPF_RET | BPF_K, 0), + }; + // clang-format on + struct sock_fprog filter = {sizeof(filter_code) / sizeof(filter_code[0]), filter_code}; + + if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter))) { + int res = errno; + ALOGE("attach packet filter failed: %s", strerror(errno)); + return -res; + } + + struct sockaddr_ll sll = { + .sll_family = AF_PACKET, + .sll_protocol = htons(ETH_P_IPV6), + .sll_ifindex = ifindex, + .sll_pkttype = + PACKET_OTHERHOST, // The 464xlat IPv6 address is not assigned to the kernel. + }; + if (bind(sock, (struct sockaddr*)&sll, sizeof(sll))) { + int res = errno; + ALOGE("binding packet socket: %s", strerror(errno)); + return -res; + } + + return 0; +} + +} // namespace clat +} // namespace net +} // namespace android diff --git a/service/native/libs/libclat/clatutils_test.cpp b/service/native/libs/libclat/clatutils_test.cpp new file mode 100644 index 0000000000..4153e19d37 --- /dev/null +++ b/service/native/libs/libclat/clatutils_test.cpp @@ -0,0 +1,187 @@ +// 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. + +#include "libclat/clatutils.h" + +#include +#include +#include +#include +#include +#include "tun_interface.h" + +extern "C" { +#include "checksum.h" +} + +// Default translation parameters. +static const char kIPv4LocalAddr[] = "192.0.0.4"; + +namespace android { +namespace net { +namespace clat { + +using android::net::TunInterface; +using base::StringPrintf; + +class ClatUtils : public ::testing::Test {}; + +// Mock functions for isIpv4AddressFree. +bool neverFree(in_addr_t /* addr */) { + return 0; +} +bool alwaysFree(in_addr_t /* addr */) { + return 1; +} +bool only2Free(in_addr_t addr) { + return (ntohl(addr) & 0xff) == 2; +} +bool over6Free(in_addr_t addr) { + return (ntohl(addr) & 0xff) >= 6; +} +bool only10Free(in_addr_t addr) { + return (ntohl(addr) & 0xff) == 10; +} + +// Apply mocked isIpv4AddressFree function for selectIpv4Address test. +in_addr_t selectIpv4Address(const in_addr ip, int16_t prefixlen, + isIpv4AddrFreeFn fn /* mocked function */) { + // Call internal function to replace isIpv4AddressFreeFn for testing. + return selectIpv4AddressInternal(ip, prefixlen, fn); +} + +TEST_F(ClatUtils, SelectIpv4Address) { + struct in_addr addr; + + inet_pton(AF_INET, kIPv4LocalAddr, &addr); + + // If no addresses are free, return INADDR_NONE. + EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 29, neverFree)); + EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 16, neverFree)); + + // If the configured address is free, pick that. But a prefix that's too big is invalid. + EXPECT_EQ(inet_addr(kIPv4LocalAddr), selectIpv4Address(addr, 29, alwaysFree)); + EXPECT_EQ(inet_addr(kIPv4LocalAddr), selectIpv4Address(addr, 20, alwaysFree)); + EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 15, alwaysFree)); + + // A prefix length of 32 works, but anything above it is invalid. + EXPECT_EQ(inet_addr(kIPv4LocalAddr), selectIpv4Address(addr, 32, alwaysFree)); + EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 33, alwaysFree)); + + // If another address is free, pick it. + EXPECT_EQ(inet_addr("192.0.0.6"), selectIpv4Address(addr, 29, over6Free)); + + // Check that we wrap around to addresses that are lower than the first address. + EXPECT_EQ(inet_addr("192.0.0.2"), selectIpv4Address(addr, 29, only2Free)); + EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 30, only2Free)); + + // If a free address exists outside the prefix, we don't pick it. + EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 29, only10Free)); + EXPECT_EQ(inet_addr("192.0.0.10"), selectIpv4Address(addr, 24, only10Free)); + + // Now try using the real function which sees if IP addresses are free using bind(). + // Assume that the machine running the test has the address 127.0.0.1, but not 8.8.8.8. + addr.s_addr = inet_addr("8.8.8.8"); + EXPECT_EQ(inet_addr("8.8.8.8"), selectIpv4Address(addr, 29)); + + addr.s_addr = inet_addr("127.0.0.1"); + EXPECT_EQ(inet_addr("127.0.0.2"), selectIpv4Address(addr, 29)); +} + +TEST_F(ClatUtils, MakeChecksumNeutral) { + // We can't test generateIPv6Address here since it requires manipulating routing, which we can't + // do without talking to the real netd on the system. + uint32_t rand = arc4random_uniform(0xffffffff); + uint16_t rand1 = rand & 0xffff; + uint16_t rand2 = (rand >> 16) & 0xffff; + std::string v6PrefixStr = StringPrintf("2001:db8:%x:%x", rand1, rand2); + std::string v6InterfaceAddrStr = StringPrintf("%s::%x:%x", v6PrefixStr.c_str(), rand2, rand1); + std::string nat64PrefixStr = StringPrintf("2001:db8:%x:%x::", rand2, rand1); + + in_addr v4 = {inet_addr(kIPv4LocalAddr)}; + in6_addr v6InterfaceAddr; + ASSERT_TRUE(inet_pton(AF_INET6, v6InterfaceAddrStr.c_str(), &v6InterfaceAddr)); + in6_addr nat64Prefix; + ASSERT_TRUE(inet_pton(AF_INET6, nat64PrefixStr.c_str(), &nat64Prefix)); + + // Generate a boatload of random IIDs. + int onebits = 0; + uint64_t prev_iid = 0; + for (int i = 0; i < 100000; i++) { + in6_addr v6 = v6InterfaceAddr; + makeChecksumNeutral(&v6, v4, nat64Prefix); + + // Check the generated IP address is in the same prefix as the interface IPv6 address. + EXPECT_EQ(0, memcmp(&v6, &v6InterfaceAddr, 8)); + + // Check that consecutive IIDs are not the same. + uint64_t iid = *(uint64_t*)(&v6.s6_addr[8]); + ASSERT_TRUE(iid != prev_iid) + << "Two consecutive random IIDs are the same: " << std::showbase << std::hex << iid + << "\n"; + prev_iid = iid; + + // Check that the IID is checksum-neutral with the NAT64 prefix and the + // local prefix. + uint16_t c1 = ip_checksum_finish(ip_checksum_add(0, &v4, sizeof(v4))); + uint16_t c2 = ip_checksum_finish(ip_checksum_add(0, &nat64Prefix, sizeof(nat64Prefix)) + + ip_checksum_add(0, &v6, sizeof(v6))); + + if (c1 != c2) { + char v6Str[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, &v6, v6Str, sizeof(v6Str)); + FAIL() << "Bad IID: " << v6Str << " not checksum-neutral with " << kIPv4LocalAddr + << " and " << nat64PrefixStr.c_str() << std::showbase << std::hex + << "\n IPv4 checksum: " << c1 << "\n IPv6 checksum: " << c2 << "\n"; + } + + // Check that IIDs are roughly random and use all the bits by counting the + // total number of bits set to 1 in a random sample of 100000 generated IIDs. + onebits += __builtin_popcountll(*(uint64_t*)&iid); + } + EXPECT_LE(3190000, onebits); + EXPECT_GE(3210000, onebits); +} + +TEST_F(ClatUtils, DetectMtu) { + // ::1 with bottom 32 bits set to 1 is still ::1 which routes via lo with mtu of 64KiB + ASSERT_EQ(detect_mtu(&in6addr_loopback, htonl(1), 0 /*MARK_UNSET*/), 65536); +} + +TEST_F(ClatUtils, ConfigurePacketSocket) { + // Create an interface for configure_packet_socket to attach socket filter to. + TunInterface v6Iface; + ASSERT_EQ(0, v6Iface.init()); + + int s = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC, htons(ETH_P_IPV6)); + EXPECT_LE(0, s); + struct in6_addr addr6; + EXPECT_EQ(1, inet_pton(AF_INET6, "2001:db8::f00", &addr6)); + EXPECT_EQ(0, configure_packet_socket(s, &addr6, v6Iface.ifindex())); + + // Check that the packet socket is bound to the interface. We can't check the socket filter + // because there is no way to fetch it from the kernel. + sockaddr_ll sll; + socklen_t len = sizeof(sll); + ASSERT_EQ(0, getsockname(s, reinterpret_cast(&sll), &len)); + EXPECT_EQ(htons(ETH_P_IPV6), sll.sll_protocol); + EXPECT_EQ(sll.sll_ifindex, v6Iface.ifindex()); + + close(s); + v6Iface.destroy(); +} + +} // namespace clat +} // namespace net +} // namespace android diff --git a/service/native/libs/libclat/include/libclat/clatutils.h b/service/native/libs/libclat/include/libclat/clatutils.h new file mode 100644 index 0000000000..812c86eb21 --- /dev/null +++ b/service/native/libs/libclat/include/libclat/clatutils.h @@ -0,0 +1,37 @@ +// 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. + +#pragma once +#include +#include + +namespace android { +namespace net { +namespace clat { + +bool isIpv4AddressFree(in_addr_t addr); +in_addr_t selectIpv4Address(const in_addr ip, int16_t prefixlen); +void makeChecksumNeutral(in6_addr* v6, const in_addr v4, const in6_addr& nat64Prefix); +int generateIpv6Address(const char* iface, const in_addr v4, const in6_addr& nat64Prefix, + in6_addr* v6); +int detect_mtu(const struct in6_addr* plat_subnet, uint32_t plat_suffix, uint32_t mark); +int configure_packet_socket(int sock, in6_addr* addr, int ifindex); + +// For testing +typedef bool (*isIpv4AddrFreeFn)(in_addr_t); +in_addr_t selectIpv4AddressInternal(const in_addr ip, int16_t prefixlen, isIpv4AddrFreeFn fn); + +} // namespace clat +} // namespace net +} // namespace android diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java new file mode 100644 index 0000000000..4d243c482a --- /dev/null +++ b/service/src/com/android/server/connectivity/ClatCoordinator.java @@ -0,0 +1,324 @@ +/* + * 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 android.net.INetd.PERMISSION_SYSTEM; + +import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.INetd; +import android.net.InterfaceConfigurationParcel; +import android.net.IpPrefix; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.net.module.util.InterfaceParams; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.net.InetAddress; +import java.nio.ByteBuffer; + +/** + * This coordinator is responsible for providing clat relevant functionality. + * + * {@hide} + */ +public class ClatCoordinator { + private static final String TAG = ClatCoordinator.class.getSimpleName(); + + // Sync from external/android-clat/clatd.c + // 40 bytes IPv6 header - 20 bytes IPv4 header + 8 bytes fragment header. + @VisibleForTesting + static final int MTU_DELTA = 28; + @VisibleForTesting + static final int CLAT_MAX_MTU = 65536; + + // This must match the interface prefix in clatd.c. + private static final String CLAT_PREFIX = "v4-"; + + // For historical reasons, start with 192.0.0.4, and after that, use all subsequent addresses + // in 192.0.0.0/29 (RFC 7335). + @VisibleForTesting + static final String INIT_V4ADDR_STRING = "192.0.0.4"; + @VisibleForTesting + static final int INIT_V4ADDR_PREFIX_LEN = 29; + private static final InetAddress GOOGLE_DNS_4 = InetAddress.parseNumericAddress("8.8.8.8"); + + private static final int INVALID_IFINDEX = 0; + private static final int INVALID_PID = 0; + + @NonNull + private final INetd mNetd; + @NonNull + private final Dependencies mDeps; + @Nullable + private String mIface = null; + private int mPid = INVALID_PID; + + @VisibleForTesting + abstract static class Dependencies { + /** + * Get netd. + */ + @NonNull + public abstract INetd getNetd(); + + /** + * @see ParcelFileDescriptor#adoptFd(int). + */ + @NonNull + public ParcelFileDescriptor adoptFd(int fd) { + return ParcelFileDescriptor.adoptFd(fd); + } + + /** + * Get interface index for a given interface. + */ + public int getInterfaceIndex(String ifName) { + final InterfaceParams params = InterfaceParams.getByName(ifName); + return params != null ? params.index : INVALID_IFINDEX; + } + + /** + * Create tun interface for a given interface name. + */ + public int createTunInterface(@NonNull String tuniface) throws IOException { + return native_createTunInterface(tuniface); + } + + /** + * Pick an IPv4 address for clat. + */ + @NonNull + public String selectIpv4Address(@NonNull String v4addr, int prefixlen) + throws IOException { + return native_selectIpv4Address(v4addr, prefixlen); + } + + /** + * Generate a checksum-neutral IID. + */ + @NonNull + public String generateIpv6Address(@NonNull String iface, @NonNull String v4, + @NonNull String prefix64) throws IOException { + return native_generateIpv6Address(iface, v4, prefix64); + } + + /** + * Detect MTU. + */ + public int detectMtu(@NonNull String platSubnet, int platSuffix, int mark) + throws IOException { + return native_detectMtu(platSubnet, platSuffix, mark); + } + + /** + * Open packet socket. + */ + public int openPacketSocket() throws IOException { + return native_openPacketSocket(); + } + + /** + * Open IPv6 raw socket and set SO_MARK. + */ + public int openRawSocket6(int mark) throws IOException { + return native_openRawSocket6(mark); + } + + /** + * Add anycast setsockopt. + */ + public void addAnycastSetsockopt(@NonNull FileDescriptor sock, String v6, int ifindex) + throws IOException { + native_addAnycastSetsockopt(sock, v6, ifindex); + } + + /** + * Configure packet socket. + */ + public void configurePacketSocket(@NonNull FileDescriptor sock, String v6, int ifindex) + throws IOException { + native_configurePacketSocket(sock, v6, ifindex); + } + } + + @VisibleForTesting + static int getFwmark(int netId) { + // See union Fwmark in system/netd/include/Fwmark.h + return (netId & 0xffff) + | 0x1 << 16 // protectedFromVpn: true + | 0x1 << 17 // explicitlySelected: true + | (PERMISSION_SYSTEM & 0x3) << 18; + } + + @VisibleForTesting + static int adjustMtu(int mtu) { + // clamp to minimum ipv6 mtu - this probably cannot ever trigger + if (mtu < IPV6_MIN_MTU) mtu = IPV6_MIN_MTU; + // clamp to buffer size + if (mtu > CLAT_MAX_MTU) mtu = CLAT_MAX_MTU; + // decrease by ipv6(40) + ipv6 fragmentation header(8) vs ipv4(20) overhead of 28 bytes + mtu -= MTU_DELTA; + + return mtu; + } + + public ClatCoordinator(@NonNull Dependencies deps) { + mDeps = deps; + mNetd = mDeps.getNetd(); + } + + /** + * Start clatd for a given interface and NAT64 prefix. + */ + public String clatStart(final String iface, final int netId, + @NonNull final IpPrefix nat64Prefix) + throws IOException { + if (nat64Prefix.getPrefixLength() != 96) { + throw new IOException("Prefix must be 96 bits long: " + nat64Prefix); + } + + // [1] Pick an IPv4 address from 192.0.0.4, 192.0.0.5, 192.0.0.6 .. + final String v4; + try { + v4 = mDeps.selectIpv4Address(INIT_V4ADDR_STRING, INIT_V4ADDR_PREFIX_LEN); + } catch (IOException e) { + throw new IOException("no IPv4 addresses were available for clat: " + e); + } + + // [2] Generate a checksum-neutral IID. + final String pfx96 = nat64Prefix.getAddress().getHostAddress(); + final String v6; + try { + v6 = mDeps.generateIpv6Address(iface, v4, pfx96); + } catch (IOException e) { + throw new IOException("no IPv6 addresses were available for clat: " + e); + } + + // [3] Open, configure and bring up the tun interface. + // Create the v4-... tun interface. + final String tunIface = CLAT_PREFIX + iface; + final ParcelFileDescriptor tunFd; + try { + tunFd = mDeps.adoptFd(mDeps.createTunInterface(tunIface)); + } catch (IOException e) { + throw new IOException("Create tun interface " + tunIface + " failed: " + e); + } + + // disable IPv6 on it - failing to do so is not a critical error + try { + mNetd.interfaceSetEnableIPv6(tunIface, false /* enabled */); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Disable IPv6 on " + tunIface + " failed: " + e); + } + + // Detect ipv4 mtu. + final Integer fwmark = getFwmark(netId); + final int detectedMtu = mDeps.detectMtu(pfx96, + ByteBuffer.wrap(GOOGLE_DNS_4.getAddress()).getInt(), fwmark); + final int mtu = adjustMtu(detectedMtu); + Log.i(TAG, "ipv4 mtu is " + mtu); + + // TODO: add setIptablesDropRule + + // Config tun interface mtu, address and bring up. + try { + mNetd.interfaceSetMtu(tunIface, mtu); + } catch (RemoteException | ServiceSpecificException e) { + throw new IOException("Set MTU " + mtu + " on " + tunIface + " failed: " + e); + } + final InterfaceConfigurationParcel ifConfig = new InterfaceConfigurationParcel(); + ifConfig.ifName = tunIface; + ifConfig.ipv4Addr = v4; + ifConfig.prefixLength = 32; + ifConfig.hwAddr = ""; + ifConfig.flags = new String[] {IF_STATE_UP}; + try { + mNetd.interfaceSetCfg(ifConfig); + } catch (RemoteException | ServiceSpecificException e) { + throw new IOException("Setting IPv4 address to " + ifConfig.ipv4Addr + "/" + + ifConfig.prefixLength + " failed on " + ifConfig.ifName + ": " + e); + } + + // [4] Open and configure local 464xlat read/write sockets. + // Opens a packet socket to receive IPv6 packets in clatd. + 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 + // 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) { + throw new IOException("Open packet socket failed: " + e); + } + + // Opens a raw socket with a given fwmark to send IPv6 packets in clatd. + final ParcelFileDescriptor writeSock6; + try { + // Use a JNI call to get native file descriptor instead of Os.socket(). See above + // reason why we use jniOpenPacketSocket6(). + writeSock6 = mDeps.adoptFd(mDeps.openRawSocket6(fwmark)); + } catch (IOException e) { + throw new IOException("Open raw socket failed: " + e); + } + + final int ifaceIndex = mDeps.getInterfaceIndex(iface); + if (ifaceIndex == INVALID_IFINDEX) { + throw new IOException("Fail to get interface index for interface " + iface); + } + + // Start translating packets to the new prefix. + try { + mDeps.addAnycastSetsockopt(writeSock6.getFileDescriptor(), v6, ifaceIndex); + } catch (IOException e) { + throw new IOException("add anycast sockopt failed: " + e); + } + + // Update our packet socket filter to reflect the new 464xlat IP address. + try { + mDeps.configurePacketSocket(readSock6.getFileDescriptor(), v6, ifaceIndex); + } catch (IOException e) { + throw new IOException("configure packet socket failed: " + e); + } + + // TODO: start clatd and returns local xlat464 v6 address. + return null; + } + + private static native String native_selectIpv4Address(String v4addr, int prefixlen) + throws IOException; + private static native String native_generateIpv6Address(String iface, String v4, + String prefix64) throws IOException; + private static native int native_createTunInterface(String tuniface) throws IOException; + private static native int native_detectMtu(String platSubnet, int platSuffix, int mark) + throws IOException; + private static native int native_openPacketSocket() throws IOException; + private static native int native_openRawSocket6(int mark) throws IOException; + private static native void native_addAnycastSetsockopt(FileDescriptor sock, String v6, + int ifindex) throws IOException; + private static native void native_configurePacketSocket(FileDescriptor sock, String v6, + int ifindex) throws IOException; +}