diff options
-rw-r--r-- | Android.bp | 22 | ||||
-rw-r--r-- | jni/network_stack_utils_jni.cpp | 264 | ||||
-rw-r--r-- | src/android/net/apf/ApfFilter.java | 3 | ||||
-rw-r--r-- | src/android/net/dhcp/DhcpClient.java | 3 | ||||
-rw-r--r-- | src/android/net/dhcp/DhcpServer.java | 3 | ||||
-rw-r--r-- | src/android/net/ip/ConnectivityPacketTracker.java | 4 | ||||
-rw-r--r-- | src/android/net/util/NetworkStackUtils.java | 41 | ||||
-rw-r--r-- | tests/Android.bp | 1 |
8 files changed, 334 insertions, 7 deletions
@@ -49,6 +49,27 @@ android_library { manifest: "AndroidManifestBase.xml", } +cc_library_shared { + name: "libnetworkstackutilsjni", + srcs: [ + "jni/network_stack_utils_jni.cpp" + ], + + shared_libs: [ + "liblog", + "libcutils", + "libnativehelper", + ], + static_libs: [ + "libpcap", + ], + cflags: [ + "-Wall", + "-Werror", + "-Wno-unused-parameter", + ], +} + java_defaults { name: "NetworkStackAppCommon", defaults: ["NetworkStackCommon"], @@ -56,6 +77,7 @@ java_defaults { static_libs: [ "NetworkStackBase", ], + jni_libs: ["libnetworkstackutilsjni"], // Resources already included in NetworkStackBase resource_dirs: [], jarjar_rules: "jarjar-rules-shared.txt", diff --git a/jni/network_stack_utils_jni.cpp b/jni/network_stack_utils_jni.cpp new file mode 100644 index 0000000..5544eaa --- /dev/null +++ b/jni/network_stack_utils_jni.cpp @@ -0,0 +1,264 @@ +/* + * Copyright 2019, 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 "NetworkStackUtils-JNI" + +#include <errno.h> +#include <jni.h> +#include <linux/filter.h> +#include <linux/if_arp.h> +#include <net/if.h> +#include <netinet/ether.h> +#include <netinet/icmp6.h> +#include <netinet/ip.h> +#include <netinet/ip6.h> +#include <netinet/udp.h> +#include <stdlib.h> + +#include <string> + +#include <nativehelper/JNIHelp.h> +#include <utils/Log.h> + +namespace android { +constexpr const char NETWORKSTACKUTILS_PKG_NAME[] = "android/net/util/NetworkStackUtils"; + +static const uint32_t kEtherTypeOffset = offsetof(ether_header, ether_type); +static const uint32_t kEtherHeaderLen = sizeof(ether_header); +static const uint32_t kIPv4Protocol = kEtherHeaderLen + offsetof(iphdr, protocol); +static const uint32_t kIPv4FlagsOffset = kEtherHeaderLen + offsetof(iphdr, frag_off); +static const uint32_t kIPv6NextHeader = kEtherHeaderLen + offsetof(ip6_hdr, ip6_nxt); +static const uint32_t kIPv6PayloadStart = kEtherHeaderLen + sizeof(ip6_hdr); +static const uint32_t kICMPv6TypeOffset = kIPv6PayloadStart + offsetof(icmp6_hdr, icmp6_type); +static const uint32_t kUDPSrcPortIndirectOffset = kEtherHeaderLen + offsetof(udphdr, source); +static const uint32_t kUDPDstPortIndirectOffset = kEtherHeaderLen + offsetof(udphdr, dest); +static const uint16_t kDhcpClientPort = 68; + +static bool checkLenAndCopy(JNIEnv* env, const jbyteArray& addr, int len, void* dst) { + if (env->GetArrayLength(addr) != len) { + return false; + } + env->GetByteArrayRegion(addr, 0, len, reinterpret_cast<jbyte*>(dst)); + return true; +} + +static void network_stack_utils_addArpEntry(JNIEnv *env, jobject thiz, jbyteArray ethAddr, + jbyteArray ipv4Addr, jstring ifname, jobject javaFd) { + arpreq req = {}; + sockaddr_in& netAddrStruct = *reinterpret_cast<sockaddr_in*>(&req.arp_pa); + sockaddr& ethAddrStruct = req.arp_ha; + + ethAddrStruct.sa_family = ARPHRD_ETHER; + if (!checkLenAndCopy(env, ethAddr, ETH_ALEN, ethAddrStruct.sa_data)) { + jniThrowException(env, "java/io/IOException", "Invalid ethAddr length"); + return; + } + + netAddrStruct.sin_family = AF_INET; + if (!checkLenAndCopy(env, ipv4Addr, sizeof(in_addr), &netAddrStruct.sin_addr)) { + jniThrowException(env, "java/io/IOException", "Invalid ipv4Addr length"); + return; + } + + int ifLen = env->GetStringLength(ifname); + // IFNAMSIZ includes the terminating NULL character + if (ifLen >= IFNAMSIZ) { + jniThrowException(env, "java/io/IOException", "ifname too long"); + return; + } + env->GetStringUTFRegion(ifname, 0, ifLen, req.arp_dev); + + req.arp_flags = ATF_COM; // Completed entry (ha valid) + int fd = jniGetFDFromFileDescriptor(env, javaFd); + if (fd < 0) { + jniThrowExceptionFmt(env, "java/io/IOException", "Invalid file descriptor"); + return; + } + // See also: man 7 arp + if (ioctl(fd, SIOCSARP, &req)) { + jniThrowExceptionFmt(env, "java/io/IOException", "ioctl error: %s", strerror(errno)); + return; + } +} + +static void network_stack_utils_attachDhcpFilter(JNIEnv *env, jobject clazz, jobject javaFd) { + static sock_filter filter_code[] = { + // Check the protocol is UDP. + BPF_STMT(BPF_LD | BPF_B | BPF_ABS, kIPv4Protocol), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_UDP, 0, 6), + + // Check this is not a fragment. + BPF_STMT(BPF_LD | BPF_H | BPF_ABS, kIPv4FlagsOffset), + BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K, IP_OFFMASK, 4, 0), + + // Get the IP header length. + BPF_STMT(BPF_LDX | BPF_B | BPF_MSH, kEtherHeaderLen), + + // Check the destination port. + BPF_STMT(BPF_LD | BPF_H | BPF_IND, kUDPDstPortIndirectOffset), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, kDhcpClientPort, 0, 1), + + // Accept or reject. + BPF_STMT(BPF_RET | BPF_K, 0xffff), + BPF_STMT(BPF_RET | BPF_K, 0) + }; + static const sock_fprog filter = { + sizeof(filter_code) / sizeof(filter_code[0]), + filter_code, + }; + + int fd = jniGetFDFromFileDescriptor(env, javaFd); + if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) { + jniThrowExceptionFmt(env, "java/net/SocketException", + "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno)); + } +} + +static void network_stack_utils_attachRaFilter(JNIEnv *env, jobject clazz, jobject javaFd, + jint hardwareAddressType) { + if (hardwareAddressType != ARPHRD_ETHER) { + jniThrowExceptionFmt(env, "java/net/SocketException", + "attachRaFilter only supports ARPHRD_ETHER"); + return; + } + + static sock_filter filter_code[] = { + // Check IPv6 Next Header is ICMPv6. + BPF_STMT(BPF_LD | BPF_B | BPF_ABS, kIPv6NextHeader), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_ICMPV6, 0, 3), + + // Check ICMPv6 type is Router Advertisement. + BPF_STMT(BPF_LD | BPF_B | BPF_ABS, kICMPv6TypeOffset), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ND_ROUTER_ADVERT, 0, 1), + + // Accept or reject. + BPF_STMT(BPF_RET | BPF_K, 0xffff), + BPF_STMT(BPF_RET | BPF_K, 0) + }; + static const sock_fprog filter = { + sizeof(filter_code) / sizeof(filter_code[0]), + filter_code, + }; + + int fd = jniGetFDFromFileDescriptor(env, javaFd); + if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) { + jniThrowExceptionFmt(env, "java/net/SocketException", + "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno)); + } +} + +// TODO: Move all this filter code into libnetutils. +static void network_stack_utils_attachControlPacketFilter( + JNIEnv *env, jobject clazz, jobject javaFd, jint hardwareAddressType) { + if (hardwareAddressType != ARPHRD_ETHER) { + jniThrowExceptionFmt(env, "java/net/SocketException", + "attachControlPacketFilter only supports ARPHRD_ETHER"); + return; + } + + // Capture all: + // - ARPs + // - DHCPv4 packets + // - Router Advertisements & Solicitations + // - Neighbor Advertisements & Solicitations + // + // tcpdump: + // arp or + // '(ip and udp port 68)' or + // '(icmp6 and ip6[40] >= 133 and ip6[40] <= 136)' + static sock_filter filter_code[] = { + // Load the link layer next payload field. + BPF_STMT(BPF_LD | BPF_H | BPF_ABS, kEtherTypeOffset), + + // Accept all ARP. + // TODO: Figure out how to better filter ARPs on noisy networks. + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ETHERTYPE_ARP, 16, 0), + + // If IPv4: + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ETHERTYPE_IP, 0, 9), + + // Check the protocol is UDP. + BPF_STMT(BPF_LD | BPF_B | BPF_ABS, kIPv4Protocol), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_UDP, 0, 14), + + // Check this is not a fragment. + BPF_STMT(BPF_LD | BPF_H | BPF_ABS, kIPv4FlagsOffset), + BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K, IP_OFFMASK, 12, 0), + + // Get the IP header length. + BPF_STMT(BPF_LDX | BPF_B | BPF_MSH, kEtherHeaderLen), + + // Check the source port. + BPF_STMT(BPF_LD | BPF_H | BPF_IND, kUDPSrcPortIndirectOffset), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, kDhcpClientPort, 8, 0), + + // Check the destination port. + BPF_STMT(BPF_LD | BPF_H | BPF_IND, kUDPDstPortIndirectOffset), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, kDhcpClientPort, 6, 7), + + // IPv6 ... + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ETHERTYPE_IPV6, 0, 6), + // ... check IPv6 Next Header is ICMPv6 (ignore fragments), ... + BPF_STMT(BPF_LD | BPF_B | BPF_ABS, kIPv6NextHeader), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_ICMPV6, 0, 4), + // ... and check the ICMPv6 type is one of RS/RA/NS/NA. + BPF_STMT(BPF_LD | BPF_B | BPF_ABS, kICMPv6TypeOffset), + BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, ND_ROUTER_SOLICIT, 0, 2), + BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, ND_NEIGHBOR_ADVERT, 1, 0), + + // Accept or reject. + BPF_STMT(BPF_RET | BPF_K, 0xffff), + BPF_STMT(BPF_RET | BPF_K, 0) + }; + static const sock_fprog filter = { + sizeof(filter_code) / sizeof(filter_code[0]), + filter_code, + }; + + int fd = jniGetFDFromFileDescriptor(env, javaFd); + if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) { + jniThrowExceptionFmt(env, "java/net/SocketException", + "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno)); + } +} + +/* + * JNI registration. + */ +static const JNINativeMethod gNetworkStackUtilsMethods[] = { + /* name, signature, funcPtr */ + { "addArpEntry", "([B[BLjava/lang/String;Ljava/io/FileDescriptor;)V", (void*) network_stack_utils_addArpEntry }, + { "attachDhcpFilter", "(Ljava/io/FileDescriptor;)V", (void*) network_stack_utils_attachDhcpFilter }, + { "attachRaFilter", "(Ljava/io/FileDescriptor;I)V", (void*) network_stack_utils_attachRaFilter }, + { "attachControlPacketFilter", "(Ljava/io/FileDescriptor;I)V", (void*) network_stack_utils_attachControlPacketFilter }, +}; + +extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { + JNIEnv *env; + if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + ALOGE("ERROR: GetEnv failed"); + return JNI_ERR; + } + + if (jniRegisterNativeMethods(env, NETWORKSTACKUTILS_PKG_NAME, + gNetworkStackUtilsMethods, NELEM(gNetworkStackUtilsMethods)) < 0) { + return JNI_ERR; + } + + return JNI_VERSION_1_6; + +} +}; // namespace android
\ No newline at end of file diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java index d2f3259..663e2f1 100644 --- a/src/android/net/apf/ApfFilter.java +++ b/src/android/net/apf/ApfFilter.java @@ -49,7 +49,6 @@ import android.net.metrics.IpConnectivityLog; import android.net.metrics.RaEvent; import android.net.util.InterfaceParams; import android.net.util.NetworkStackUtils; -import android.net.util.SocketUtils; import android.os.PowerManager; import android.os.SystemClock; import android.system.ErrnoException; @@ -478,7 +477,7 @@ public class ApfFilter { SocketAddress addr = makePacketSocketAddress( (short) ETH_P_IPV6, mInterfaceParams.index); Os.bind(socket, addr); - SocketUtils.attachRaFilter(socket, mApfCapabilities.apfPacketFormat); + NetworkStackUtils.attachRaFilter(socket, mApfCapabilities.apfPacketFormat); } catch(SocketException|ErrnoException e) { Log.e(TAG, "Error starting filter", e); return; diff --git a/src/android/net/dhcp/DhcpClient.java b/src/android/net/dhcp/DhcpClient.java index 79d6a55..64adc0d 100644 --- a/src/android/net/dhcp/DhcpClient.java +++ b/src/android/net/dhcp/DhcpClient.java @@ -51,6 +51,7 @@ import android.net.metrics.DhcpClientEvent; import android.net.metrics.DhcpErrorEvent; import android.net.metrics.IpConnectivityLog; import android.net.util.InterfaceParams; +import android.net.util.NetworkStackUtils; import android.net.util.SocketUtils; import android.os.Message; import android.os.SystemClock; @@ -319,7 +320,7 @@ public class DhcpClient extends StateMachine { mPacketSock = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IP); SocketAddress addr = makePacketSocketAddress((short) ETH_P_IP, mIface.index); Os.bind(mPacketSock, addr); - SocketUtils.attachDhcpFilter(mPacketSock); + NetworkStackUtils.attachDhcpFilter(mPacketSock); } catch(SocketException|ErrnoException e) { Log.e(TAG, "Error creating packet socket", e); return false; diff --git a/src/android/net/dhcp/DhcpServer.java b/src/android/net/dhcp/DhcpServer.java index cd993e9..8832eaa 100644 --- a/src/android/net/dhcp/DhcpServer.java +++ b/src/android/net/dhcp/DhcpServer.java @@ -45,6 +45,7 @@ import android.annotation.Nullable; import android.net.INetworkStackStatusCallback; import android.net.MacAddress; import android.net.TrafficStats; +import android.net.util.NetworkStackUtils; import android.net.util.SharedLog; import android.net.util.SocketUtils; import android.os.Handler; @@ -207,7 +208,7 @@ public class DhcpServer extends IDhcpServer.Stub { @Override public void addArpEntry(@NonNull Inet4Address ipv4Addr, @NonNull MacAddress ethAddr, @NonNull String ifname, @NonNull FileDescriptor fd) throws IOException { - SocketUtils.addArpEntry(ipv4Addr, ethAddr, ifname, fd); + NetworkStackUtils.addArpEntry(ipv4Addr, ethAddr, ifname, fd); } @Override diff --git a/src/android/net/ip/ConnectivityPacketTracker.java b/src/android/net/ip/ConnectivityPacketTracker.java index de54824..eb49218 100644 --- a/src/android/net/ip/ConnectivityPacketTracker.java +++ b/src/android/net/ip/ConnectivityPacketTracker.java @@ -25,8 +25,8 @@ import static android.system.OsConstants.SOCK_RAW; import android.net.util.ConnectivityPacketSummary; import android.net.util.InterfaceParams; +import android.net.util.NetworkStackUtils; import android.net.util.PacketReader; -import android.net.util.SocketUtils; import android.os.Handler; import android.system.ErrnoException; import android.system.Os; @@ -103,7 +103,7 @@ public class ConnectivityPacketTracker { FileDescriptor s = null; try { s = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0); - SocketUtils.attachControlPacketFilter(s, ARPHRD_ETHER); + NetworkStackUtils.attachControlPacketFilter(s, ARPHRD_ETHER); Os.bind(s, makePacketSocketAddress((short) ETH_P_ALL, mInterface.index)); } catch (ErrnoException | IOException e) { logError("Failed to create packet tracking socket: ", e); diff --git a/src/android/net/util/NetworkStackUtils.java b/src/android/net/util/NetworkStackUtils.java index 670563c..ee83a85 100644 --- a/src/android/net/util/NetworkStackUtils.java +++ b/src/android/net/util/NetworkStackUtils.java @@ -22,14 +22,18 @@ import android.util.SparseArray; import java.io.FileDescriptor; import java.io.IOException; +import java.net.Inet4Address; +import java.net.SocketException; import java.util.List; import java.util.function.Predicate; - /** * Collection of utilities for the network stack. */ public class NetworkStackUtils { + static { + System.loadLibrary("networkstackutilsjni"); + } /** * @return True if the array is null or 0-length. @@ -97,4 +101,39 @@ public class NetworkStackUtils { // TODO: Link to DeviceConfig API once it is ready. return defaultValue; } + + /** + * Attaches a socket filter that accepts DHCP packets to the given socket. + */ + public static native void attachDhcpFilter(FileDescriptor fd) throws SocketException; + + /** + * Attaches a socket filter that accepts ICMPv6 router advertisements to the given socket. + * @param fd the socket's {@link FileDescriptor}. + * @param packetType the hardware address type, one of ARPHRD_*. + */ + public static native void attachRaFilter(FileDescriptor fd, int packetType) + throws SocketException; + + /** + * Attaches a socket filter that accepts L2-L4 signaling traffic required for IP connectivity. + * + * This includes: all ARP, ICMPv6 RS/RA/NS/NA messages, and DHCPv4 exchanges. + * + * @param fd the socket's {@link FileDescriptor}. + * @param packetType the hardware address type, one of ARPHRD_*. + */ + public static native void attachControlPacketFilter(FileDescriptor fd, int packetType) + throws SocketException; + + /** + * Add an entry into the ARP cache. + */ + public static void addArpEntry(Inet4Address ipv4Addr, android.net.MacAddress ethAddr, + String ifname, FileDescriptor fd) throws IOException { + addArpEntry(ethAddr.toByteArray(), ipv4Addr.getAddress(), ifname, fd); + } + + private static native void addArpEntry(byte[] ethAddr, byte[] netAddr, String ifname, + FileDescriptor fd) throws IOException; } diff --git a/tests/Android.bp b/tests/Android.bp index aadf99e..8945a1c 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -54,6 +54,7 @@ android_test { "liblzma", "libnativehelper", "libnetworkstacktestsjni", + "libnetworkstackutilsjni", "libpackagelistparser", "libpcre2", "libprocessgroup", |