diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java index 4e615a197e..f27c831689 100644 --- a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java +++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java @@ -158,6 +158,18 @@ public class BpfCoordinatorShimImpl return true; } + @Override + public boolean attachProgram(String iface, boolean downstream) { + /* no op */ + return true; + } + + @Override + public boolean detachProgram(String iface) { + /* no op */ + return true; + } + @Override public String toString() { return "Netd used"; diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java index 03d24433d2..4f7fe65a6a 100644 --- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java +++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java @@ -31,6 +31,7 @@ import androidx.annotation.Nullable; import com.android.networkstack.tethering.BpfCoordinator.Dependencies; import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule; import com.android.networkstack.tethering.BpfMap; +import com.android.networkstack.tethering.BpfUtils; import com.android.networkstack.tethering.Tether4Key; import com.android.networkstack.tethering.Tether4Value; import com.android.networkstack.tethering.Tether6Value; @@ -42,6 +43,7 @@ import com.android.networkstack.tethering.TetherStatsValue; import com.android.networkstack.tethering.TetherUpstream6Key; import java.io.FileDescriptor; +import java.io.IOException; /** * Bpf coordinator class for API shims. @@ -358,6 +360,32 @@ public class BpfCoordinatorShimImpl return true; } + @Override + public boolean attachProgram(String iface, boolean downstream) { + if (!isInitialized()) return false; + + try { + BpfUtils.attachProgram(iface, downstream); + } catch (IOException e) { + mLog.e("Could not attach program: " + e); + return false; + } + return true; + } + + @Override + public boolean detachProgram(String iface) { + if (!isInitialized()) return false; + + try { + BpfUtils.detachProgram(iface); + } catch (IOException e) { + mLog.e("Could not detach program: " + e); + return false; + } + return true; + } + private String mapStatus(BpfMap m, String name) { return name + "{" + (m != null ? "OK" : "ERROR") + "}"; } diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java index c61c44902f..b7b4c47cc3 100644 --- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java +++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java @@ -143,5 +143,19 @@ public abstract class BpfCoordinatorShim { * Deletes a tethering IPv4 offload rule from the appropriate BPF map. */ public abstract boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key); + + /** + * Attach BPF program. + * + * TODO: consider using InterfaceParams to replace interface name. + */ + public abstract boolean attachProgram(@NonNull String iface, boolean downstream); + + /** + * Detach BPF program. + * + * TODO: consider using InterfaceParams to replace interface name. + */ + public abstract boolean detachProgram(@NonNull String iface); } diff --git a/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp b/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp new file mode 100644 index 0000000000..308dfb9c98 --- /dev/null +++ b/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2021 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 +#include +#include +#include + +// TODO: use unique_fd. +#define BPF_FD_JUST_USE_INT +#include "BpfSyscallWrappers.h" +#include "bpf_tethering.h" +#include "nativehelper/scoped_utf_chars.h" + +// The maximum length of TCA_BPF_NAME. Sync from net/sched/cls_bpf.c. +#define CLS_BPF_NAME_LEN 256 + +namespace android { +// Sync from system/netd/server/NetlinkCommands.h +const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK; +const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0}; + +// TODO: move to frameworks/libs/net/common/native for sharing with +// system/netd/server/OffloadUtils.{c, h}. +static void sendAndProcessNetlinkResponse(JNIEnv* env, const void* req, int len) { + int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); // TODO: use unique_fd + if (fd == -1) { + jniThrowExceptionFmt(env, "java/io/IOException", + "socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE): %s", + strerror(errno)); + return; + } + + static constexpr int on = 1; + if (setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on))) { + jniThrowExceptionFmt(env, "java/io/IOException", + "setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, %d)", on); + close(fd); + return; + } + + // this is needed to get valid strace netlink parsing, it allocates the pid + if (bind(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR))) { + jniThrowExceptionFmt(env, "java/io/IOException", "bind(fd, {AF_NETLINK, 0, 0}): %s", + strerror(errno)); + close(fd); + return; + } + + // we do not want to receive messages from anyone besides the kernel + if (connect(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR))) { + jniThrowExceptionFmt(env, "java/io/IOException", "connect(fd, {AF_NETLINK, 0, 0}): %s", + strerror(errno)); + close(fd); + return; + } + + int rv = send(fd, req, len, 0); + + if (rv == -1) { + jniThrowExceptionFmt(env, "java/io/IOException", "send(fd, req, len, 0): %s", + strerror(errno)); + close(fd); + return; + } + + if (rv != len) { + jniThrowExceptionFmt(env, "java/io/IOException", "send(fd, req, len, 0): %s", + strerror(EMSGSIZE)); + close(fd); + return; + } + + struct { + nlmsghdr h; + nlmsgerr e; + char buf[256]; + } resp = {}; + + rv = recv(fd, &resp, sizeof(resp), MSG_TRUNC); + + if (rv == -1) { + jniThrowExceptionFmt(env, "java/io/IOException", "recv() failed: %s", strerror(errno)); + close(fd); + return; + } + + if (rv < (int)NLMSG_SPACE(sizeof(struct nlmsgerr))) { + jniThrowExceptionFmt(env, "java/io/IOException", "recv() returned short packet: %d", rv); + close(fd); + return; + } + + if (resp.h.nlmsg_len != (unsigned)rv) { + jniThrowExceptionFmt(env, "java/io/IOException", + "recv() returned invalid header length: %d != %d", resp.h.nlmsg_len, + rv); + close(fd); + return; + } + + if (resp.h.nlmsg_type != NLMSG_ERROR) { + jniThrowExceptionFmt(env, "java/io/IOException", + "recv() did not return NLMSG_ERROR message: %d", resp.h.nlmsg_type); + close(fd); + return; + } + + if (resp.e.error) { // returns 0 on success + jniThrowExceptionFmt(env, "java/io/IOException", "NLMSG_ERROR message return error: %s", + strerror(-resp.e.error)); + } + close(fd); + return; +} + +static int hardwareAddressType(const char* interface) { + int fd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (fd < 0) return -errno; + + struct ifreq ifr = {}; + // We use strncpy() instead of strlcpy() since kernel has to be able + // to handle non-zero terminated junk passed in by userspace anyway, + // and this way too long interface names (more than IFNAMSIZ-1 = 15 + // characters plus terminating NULL) will not get truncated to 15 + // characters and zero-terminated and thus potentially erroneously + // match a truncated interface if one were to exist. + strncpy(ifr.ifr_name, interface, sizeof(ifr.ifr_name)); + + int rv; + if (ioctl(fd, SIOCGIFHWADDR, &ifr, sizeof(ifr))) { + rv = -errno; + } else { + rv = ifr.ifr_hwaddr.sa_family; + } + + close(fd); + return rv; +} + +static jboolean com_android_networkstack_tethering_BpfUtils_isEthernet(JNIEnv* env, jobject clazz, + jstring iface) { + ScopedUtfChars interface(env, iface); + + int rv = hardwareAddressType(interface.c_str()); + if (rv < 0) { + jniThrowExceptionFmt(env, "java/io/IOException", + "Get hardware address type of interface %s failed: %s", + interface.c_str(), strerror(-rv)); + return false; + } + + switch (rv) { + case ARPHRD_ETHER: + return true; + case ARPHRD_NONE: + case ARPHRD_RAWIP: // in Linux 4.14+ rmnet support was upstreamed and this is 519 + case 530: // this is ARPHRD_RAWIP on some Android 4.9 kernels with rmnet + return false; + default: + jniThrowExceptionFmt(env, "java/io/IOException", + "Unknown hardware address type %s on interface %s", rv, + interface.c_str()); + return false; + } +} + +// tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/... +// direct-action +static void com_android_networkstack_tethering_BpfUtils_tcFilterAddDevBpf( + JNIEnv* env, jobject clazz, jint ifIndex, jboolean ingress, jshort prio, jshort proto, + jstring bpfProgPath) { + ScopedUtfChars pathname(env, bpfProgPath); + + const int bpfFd = bpf::retrieveProgram(pathname.c_str()); + if (bpfFd == -1) { + jniThrowExceptionFmt(env, "java/io/IOException", "retrieveProgram failed %s", + strerror(errno)); + return; + } + + struct { + nlmsghdr n; + tcmsg t; + struct { + nlattr attr; + // The maximum classifier name length is defined as IFNAMSIZ. + // See tcf_proto_ops in include/net/sch_generic.h. + char str[NLMSG_ALIGN(IFNAMSIZ)]; + } kind; + struct { + nlattr attr; + struct { + nlattr attr; + __u32 u32; + } fd; + struct { + nlattr attr; + char str[NLMSG_ALIGN(CLS_BPF_NAME_LEN)]; + } name; + struct { + nlattr attr; + __u32 u32; + } flags; + } options; + } req = { + .n = + { + .nlmsg_len = sizeof(req), + .nlmsg_type = RTM_NEWTFILTER, + .nlmsg_flags = NETLINK_REQUEST_FLAGS | NLM_F_EXCL | NLM_F_CREATE, + }, + .t = + { + .tcm_family = AF_UNSPEC, + .tcm_ifindex = ifIndex, + .tcm_handle = TC_H_UNSPEC, + .tcm_parent = TC_H_MAKE(TC_H_CLSACT, + ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS), + .tcm_info = static_cast<__u32>((static_cast(prio) << 16) | + htons(static_cast(proto))), + }, + .kind = + { + .attr = + { + .nla_len = sizeof(req.kind), + .nla_type = TCA_KIND, + }, + // Classifier name. See cls_bpf_ops in net/sched/cls_bpf.c. + .str = "bpf", + }, + .options = + { + .attr = + { + .nla_len = sizeof(req.options), + .nla_type = NLA_F_NESTED | TCA_OPTIONS, + }, + .fd = + { + .attr = + { + .nla_len = sizeof(req.options.fd), + .nla_type = TCA_BPF_FD, + }, + .u32 = static_cast<__u32>(bpfFd), + }, + .name = + { + .attr = + { + .nla_len = sizeof(req.options.name), + .nla_type = TCA_BPF_NAME, + }, + // Visible via 'tc filter show', but + // is overwritten by strncpy below + .str = "placeholder", + }, + .flags = + { + .attr = + { + .nla_len = sizeof(req.options.flags), + .nla_type = TCA_BPF_FLAGS, + }, + .u32 = TCA_BPF_FLAG_ACT_DIRECT, + }, + }, + }; + + snprintf(req.options.name.str, sizeof(req.options.name.str), "%s:[*fsobj]", + basename(pathname.c_str())); + + // The exception may be thrown from sendAndProcessNetlinkResponse. Close the file descriptor of + // BPF program before returning the function in any case. + sendAndProcessNetlinkResponse(env, &req, sizeof(req)); + close(bpfFd); +} + +// tc filter del dev .. in/egress prio .. protocol .. +static void com_android_networkstack_tethering_BpfUtils_tcFilterDelDev(JNIEnv* env, jobject clazz, + jint ifIndex, + jboolean ingress, + jshort prio, jshort proto) { + const struct { + nlmsghdr n; + tcmsg t; + } req = { + .n = + { + .nlmsg_len = sizeof(req), + .nlmsg_type = RTM_DELTFILTER, + .nlmsg_flags = NETLINK_REQUEST_FLAGS, + }, + .t = + { + .tcm_family = AF_UNSPEC, + .tcm_ifindex = ifIndex, + .tcm_handle = TC_H_UNSPEC, + .tcm_parent = TC_H_MAKE(TC_H_CLSACT, + ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS), + .tcm_info = static_cast<__u32>((static_cast(prio) << 16) | + htons(static_cast(proto))), + }, + }; + + sendAndProcessNetlinkResponse(env, &req, sizeof(req)); +} + +/* + * JNI registration. + */ +static const JNINativeMethod gMethods[] = { + /* name, signature, funcPtr */ + {"isEthernet", "(Ljava/lang/String;)Z", + (void*)com_android_networkstack_tethering_BpfUtils_isEthernet}, + {"tcFilterAddDevBpf", "(IZSSLjava/lang/String;)V", + (void*)com_android_networkstack_tethering_BpfUtils_tcFilterAddDevBpf}, + {"tcFilterDelDev", "(IZSS)V", + (void*)com_android_networkstack_tethering_BpfUtils_tcFilterDelDev}, +}; + +int register_com_android_networkstack_tethering_BpfUtils(JNIEnv* env) { + return jniRegisterNativeMethods(env, "com/android/networkstack/tethering/BpfUtils", gMethods, + NELEM(gMethods)); +} + +}; // namespace android diff --git a/Tethering/jni/onload.cpp b/Tethering/jni/onload.cpp index e31da60ef5..02e602d99e 100644 --- a/Tethering/jni/onload.cpp +++ b/Tethering/jni/onload.cpp @@ -25,6 +25,7 @@ namespace android { int register_android_net_util_TetheringUtils(JNIEnv* env); int register_com_android_networkstack_tethering_BpfMap(JNIEnv* env); int register_com_android_networkstack_tethering_BpfCoordinator(JNIEnv* env); +int register_com_android_networkstack_tethering_BpfUtils(JNIEnv* env); extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { JNIEnv *env; @@ -39,6 +40,8 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { if (register_com_android_networkstack_tethering_BpfCoordinator(env) < 0) return JNI_ERR; + if (register_com_android_networkstack_tethering_BpfUtils(env) < 0) return JNI_ERR; + return JNI_VERSION_1_6; } diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java index 194737af21..e5380e008d 100644 --- a/Tethering/src/android/net/ip/IpServer.java +++ b/Tethering/src/android/net/ip/IpServer.java @@ -1291,6 +1291,7 @@ public class IpServer extends StateMachine { // Sometimes interfaces are gone before we get // to remove their rules, which generates errors. // Just do the best we can. + mBpfCoordinator.maybeDetachProgram(mIfaceName, upstreamIface); try { mNetd.ipfwdRemoveInterfaceForward(mIfaceName, upstreamIface); } catch (RemoteException | ServiceSpecificException e) { @@ -1334,6 +1335,7 @@ public class IpServer extends StateMachine { mUpstreamIfaceSet = newUpstreamIfaceSet; for (String ifname : added) { + mBpfCoordinator.maybeAttachProgram(mIfaceName, ifname); try { mNetd.tetherAddForward(mIfaceName, ifname); mNetd.ipfwdAddInterfaceForward(mIfaceName, ifname); diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java index 7c5271678f..8df3045cdd 100644 --- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java +++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java @@ -28,6 +28,8 @@ import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED import static android.system.OsConstants.ETH_P_IP; import static android.system.OsConstants.ETH_P_IPV6; +import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM; +import static com.android.networkstack.tethering.BpfUtils.UPSTREAM; import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS; import android.app.usage.NetworkStatsManager; @@ -92,9 +94,6 @@ public class BpfCoordinator { System.loadLibrary("tetherutilsjni"); } - static final boolean DOWNSTREAM = true; - static final boolean UPSTREAM = false; - private static final String TAG = BpfCoordinator.class.getSimpleName(); private static final int DUMP_TIMEOUT_MS = 10_000; private static final MacAddress NULL_MAC_ADDRESS = MacAddress.fromString( @@ -118,7 +117,6 @@ public class BpfCoordinator { return makeMapPath((downstream ? "downstream" : "upstream") + ipVersion); } - @VisibleForTesting enum StatsType { STATS_PER_IFACE, @@ -218,6 +216,9 @@ public class BpfCoordinator { // is okay for now because there have only one upstream generally. private final HashMap mIpv4UpstreamIndices = new HashMap<>(); + // Map for upstream and downstream pair. + private final HashMap> mForwardingPairs = new HashMap<>(); + // Runnable that used by scheduling next polling of stats. private final Runnable mScheduledPollingTask = () -> { updateForwardedStats(); @@ -691,6 +692,37 @@ public class BpfCoordinator { } } + /** + * Attach BPF program + * + * TODO: consider error handling if the attach program failed. + */ + public void maybeAttachProgram(@NonNull String intIface, @NonNull String extIface) { + if (forwardingPairExists(intIface, extIface)) return; + + boolean firstDownstreamForThisUpstream = !isAnyForwardingPairOnUpstream(extIface); + forwardingPairAdd(intIface, extIface); + + mBpfCoordinatorShim.attachProgram(intIface, UPSTREAM); + // Attach if the upstream is the first time to be used in a forwarding pair. + if (firstDownstreamForThisUpstream) { + mBpfCoordinatorShim.attachProgram(extIface, DOWNSTREAM); + } + } + + /** + * Detach BPF program + */ + public void maybeDetachProgram(@NonNull String intIface, @NonNull String extIface) { + forwardingPairRemove(intIface, extIface); + + // Detaching program may fail because the interface has been removed already. + mBpfCoordinatorShim.detachProgram(intIface); + // Detach if no more forwarding pair is using the upstream. + if (!isAnyForwardingPairOnUpstream(extIface)) { + mBpfCoordinatorShim.detachProgram(extIface); + } + } // TODO: make mInterfaceNames accessible to the shim and move this code to there. private String getIfName(long ifindex) { @@ -1227,6 +1259,33 @@ public class BpfCoordinator { return false; } + private void forwardingPairAdd(@NonNull String intIface, @NonNull String extIface) { + if (!mForwardingPairs.containsKey(extIface)) { + mForwardingPairs.put(extIface, new HashSet()); + } + mForwardingPairs.get(extIface).add(intIface); + } + + private void forwardingPairRemove(@NonNull String intIface, @NonNull String extIface) { + HashSet downstreams = mForwardingPairs.get(extIface); + if (downstreams == null) return; + if (!downstreams.remove(intIface)) return; + + if (downstreams.isEmpty()) { + mForwardingPairs.remove(extIface); + } + } + + private boolean forwardingPairExists(@NonNull String intIface, @NonNull String extIface) { + if (!mForwardingPairs.containsKey(extIface)) return false; + + return mForwardingPairs.get(extIface).contains(intIface); + } + + private boolean isAnyForwardingPairOnUpstream(@NonNull String extIface) { + return mForwardingPairs.containsKey(extIface); + } + @NonNull private NetworkStats buildNetworkStats(@NonNull StatsType type, int ifIndex, @NonNull final ForwardedStats diff) { diff --git a/Tethering/src/com/android/networkstack/tethering/BpfUtils.java b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java new file mode 100644 index 0000000000..289452c783 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2021 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.networkstack.tethering; + +import static android.system.OsConstants.ETH_P_IP; +import static android.system.OsConstants.ETH_P_IPV6; + +import android.net.util.InterfaceParams; + +import androidx.annotation.NonNull; + +import java.io.IOException; + +/** + * The classes and the methods for BPF utilization. + * + * {@hide} + */ +public class BpfUtils { + static { + System.loadLibrary("tetherutilsjni"); + } + + // For better code clarity when used for 'bool ingress' parameter. + static final boolean EGRESS = false; + static final boolean INGRESS = true; + + // For better code clarify when used for 'bool downstream' parameter. + // + // This is talking about the direction of travel of the offloaded packets. + // + // Upstream means packets heading towards the internet/uplink (upload), + // thus for tethering this is attached to ingress on the downstream interface, + // while for clat this is attached to egress on the v4-* clat interface. + // + // Downstream means packets coming from the internet/uplink (download), thus + // for both clat and tethering this is attached to ingress on the upstream interface. + static final boolean DOWNSTREAM = true; + static final boolean UPSTREAM = false; + + // The priority of clat/tether hooks - smaller is higher priority. + // TC tether is higher priority then TC clat to match XDP winning over TC. + // Sync from system/netd/server/OffloadUtils.h. + static final short PRIO_TETHER6 = 1; + static final short PRIO_TETHER4 = 2; + static final short PRIO_CLAT = 3; + + private static String makeProgPath(boolean downstream, int ipVersion, boolean ether) { + String path = "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_" + + (downstream ? "downstream" : "upstream") + + ipVersion + "_" + + (ether ? "ether" : "rawip"); + return path; + } + + /** + * Attach BPF program + * + * TODO: use interface index to replace interface name. + */ + public static void attachProgram(@NonNull String iface, boolean downstream) + throws IOException { + final InterfaceParams params = InterfaceParams.getByName(iface); + if (params == null) { + throw new IOException("Fail to get interface params for interface " + iface); + } + + boolean ether; + try { + ether = isEthernet(iface); + } catch (IOException e) { + throw new IOException("isEthernet(" + params.index + "[" + iface + "]) failure: " + e); + } + + try { + // tc filter add dev .. ingress prio 1 protocol ipv6 bpf object-pinned /sys/fs/bpf/... + // direct-action + tcFilterAddDevBpf(params.index, INGRESS, PRIO_TETHER6, (short) ETH_P_IPV6, + makeProgPath(downstream, 6, ether)); + } catch (IOException e) { + throw new IOException("tc filter add dev (" + params.index + "[" + iface + + "]) ingress prio PRIO_TETHER6 protocol ipv6 failure: " + e); + } + + try { + // tc filter add dev .. ingress prio 2 protocol ip bpf object-pinned /sys/fs/bpf/... + // direct-action + tcFilterAddDevBpf(params.index, INGRESS, PRIO_TETHER4, (short) ETH_P_IP, + makeProgPath(downstream, 4, ether)); + } catch (IOException e) { + throw new IOException("tc filter add dev (" + params.index + "[" + iface + + "]) ingress prio PRIO_TETHER4 protocol ip failure: " + e); + } + } + + /** + * Detach BPF program + * + * TODO: use interface index to replace interface name. + */ + public static void detachProgram(@NonNull String iface) throws IOException { + final InterfaceParams params = InterfaceParams.getByName(iface); + if (params == null) { + throw new IOException("Fail to get interface params for interface " + iface); + } + + try { + // tc filter del dev .. ingress prio 1 protocol ipv6 + tcFilterDelDev(params.index, INGRESS, PRIO_TETHER6, (short) ETH_P_IPV6); + } catch (IOException e) { + throw new IOException("tc filter del dev (" + params.index + "[" + iface + + "]) ingress prio PRIO_TETHER6 protocol ipv6 failure: " + e); + } + + try { + // tc filter del dev .. ingress prio 2 protocol ip + tcFilterDelDev(params.index, INGRESS, PRIO_TETHER4, (short) ETH_P_IP); + } catch (IOException e) { + throw new IOException("tc filter del dev (" + params.index + "[" + iface + + "]) ingress prio PRIO_TETHER4 protocol ip failure: " + e); + } + } + + private static native boolean isEthernet(String iface) throws IOException; + + private static native void tcFilterAddDevBpf(int ifIndex, boolean ingress, short prio, + short proto, String bpfProgPath) throws IOException; + + private static native void tcFilterDelDev(int ifIndex, boolean ingress, short prio, + short proto) throws IOException; +} diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java index b45db7edff..adf1f671ca 100644 --- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java +++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java @@ -247,7 +247,7 @@ public class IpServerTest { lp.setInterfaceName(upstreamIface); dispatchTetherConnectionChanged(upstreamIface, lp, 0); } - reset(mNetd, mCallback, mAddressCoordinator); + reset(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator); when(mAddressCoordinator.requestDownstreamAddress(any(), anyBoolean())).thenReturn( mTestAddress); } @@ -471,10 +471,14 @@ public class IpServerTest { // Telling the state machine about its upstream interface triggers // a little more configuration. dispatchTetherConnectionChanged(UPSTREAM_IFACE); - InOrder inOrder = inOrder(mNetd); + InOrder inOrder = inOrder(mNetd, mBpfCoordinator); + + // Add the forwarding pair . + inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE); - verifyNoMoreInteractions(mNetd, mCallback); + + verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator); } @Test @@ -482,12 +486,19 @@ public class IpServerTest { initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE); dispatchTetherConnectionChanged(UPSTREAM_IFACE2); - InOrder inOrder = inOrder(mNetd); + InOrder inOrder = inOrder(mNetd, mBpfCoordinator); + + // Remove the forwarding pair . + inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE); + + // Add the forwarding pair . + inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2); inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2); inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2); - verifyNoMoreInteractions(mNetd, mCallback); + + verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator); } @Test @@ -497,10 +508,20 @@ public class IpServerTest { doThrow(RemoteException.class).when(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2); dispatchTetherConnectionChanged(UPSTREAM_IFACE2); - InOrder inOrder = inOrder(mNetd); + InOrder inOrder = inOrder(mNetd, mBpfCoordinator); + + // Remove the forwarding pair . + inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE); + + // Add the forwarding pair and expect that failed on + // tetherAddForward. + inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2); inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2); + + // Remove the forwarding pair to fallback. + inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE2); inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2); inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE2); } @@ -513,11 +534,21 @@ public class IpServerTest { IFACE_NAME, UPSTREAM_IFACE2); dispatchTetherConnectionChanged(UPSTREAM_IFACE2); - InOrder inOrder = inOrder(mNetd); + InOrder inOrder = inOrder(mNetd, mBpfCoordinator); + + // Remove the forwarding pair . + inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE); + + // Add the forwarding pair and expect that failed on + // ipfwdAddInterfaceForward. + inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2); inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2); inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2); + + // Remove the forwarding pair to fallback. + inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE2); inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2); inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE2); } @@ -527,19 +558,22 @@ public class IpServerTest { initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE); dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED); - InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator); + InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator); + inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE); + inOrder.verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer); inOrder.verify(mNetd).tetherApplyDnsInterfaces(); inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME); inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME); inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> IFACE_NAME.equals(cfg.ifName))); inOrder.verify(mAddressCoordinator).releaseDownstream(any()); + inOrder.verify(mBpfCoordinator).stopMonitoring(mIpServer); inOrder.verify(mCallback).updateInterfaceState( mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR); inOrder.verify(mCallback).updateLinkProperties( eq(mIpServer), any(LinkProperties.class)); - verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator); + verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator); } @Test diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java index 64ae983505..ba4ed4701b 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java @@ -26,9 +26,12 @@ import static android.net.NetworkStats.UID_TETHERING; import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED; import static android.system.OsConstants.ETH_P_IPV6; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker; import static com.android.networkstack.tethering.BpfCoordinator.StatsType; import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_IFACE; import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_UID; +import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM; +import static com.android.networkstack.tethering.BpfUtils.UPSTREAM; import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS; import static org.junit.Assert.assertEquals; @@ -70,6 +73,7 @@ import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.net.module.util.NetworkStackConstants; import com.android.net.module.util.Struct; import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule; @@ -84,6 +88,7 @@ import org.mockito.ArgumentMatcher; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; import java.net.Inet6Address; import java.net.InetAddress; @@ -1042,6 +1047,59 @@ public class BpfCoordinatorTest { verify(mBpfLimitMap).clear(); } + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testAttachDetachBpfProgram() throws Exception { + setupFunctioningNetdInterface(); + + // Static mocking for BpfUtils. + MockitoSession mockSession = ExtendedMockito.mockitoSession() + .mockStatic(BpfUtils.class) + .startMocking(); + try { + final String intIface1 = "wlan1"; + final String intIface2 = "rndis0"; + final String extIface = "rmnet_data0"; + final BpfUtils mockMarkerBpfUtils = staticMockMarker(BpfUtils.class); + final BpfCoordinator coordinator = makeBpfCoordinator(); + + // [1] Add the forwarding pair . Expect that attach both wlan1 and + // rmnet_data0. + coordinator.maybeAttachProgram(intIface1, extIface); + ExtendedMockito.verify(() -> BpfUtils.attachProgram(extIface, DOWNSTREAM)); + ExtendedMockito.verify(() -> BpfUtils.attachProgram(intIface1, UPSTREAM)); + ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); + ExtendedMockito.clearInvocations(mockMarkerBpfUtils); + + // [2] Add the forwarding pair again. Expect no more action. + coordinator.maybeAttachProgram(intIface1, extIface); + ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); + ExtendedMockito.clearInvocations(mockMarkerBpfUtils); + + // [3] Add the forwarding pair . Expect that attach rndis0 only. + coordinator.maybeAttachProgram(intIface2, extIface); + ExtendedMockito.verify(() -> BpfUtils.attachProgram(intIface2, UPSTREAM)); + ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); + ExtendedMockito.clearInvocations(mockMarkerBpfUtils); + + // [4] Remove the forwarding pair . Expect detach rndis0 only. + coordinator.maybeDetachProgram(intIface2, extIface); + ExtendedMockito.verify(() -> BpfUtils.detachProgram(intIface2)); + ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); + ExtendedMockito.clearInvocations(mockMarkerBpfUtils); + + // [5] Remove the forwarding pair . Expect that detach both wlan1 + // and rmnet_data0. + coordinator.maybeDetachProgram(intIface1, extIface); + ExtendedMockito.verify(() -> BpfUtils.detachProgram(extIface)); + ExtendedMockito.verify(() -> BpfUtils.detachProgram(intIface1)); + ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); + ExtendedMockito.clearInvocations(mockMarkerBpfUtils); + } finally { + mockSession.finishMocking(); + } + } + @Test public void testTetheringConfigSetPollingInterval() throws Exception { setupFunctioningNetdInterface();