Move static utils to the static library
These classes used to live as a static utility in the
NetworkStack module, but that's not the right place for
them.
The point of this patch is to *not* require topics, as
any change in this space will very quickly spin out of
control and become unmanageable. As such, this starts
with creating equivalent classes in a single, easier to
manage change. Followup changes will migrate users of
the old classes to use these ones instead. Finally, the
old classes can be removed. This way, work can be
broken down into separate changes and be checked in
little by little, rather than one huge topic with many
changes doing everything in one go, which is unlikely
to be manageable.
There are no code changes from the originals, but a
number of reorganizations, most of them unavoidable.
• Vertical spacing (these classes fix it)
• Package names/imports are adjusted
• Added @hide on NetlinkMonitor
• Move all contents of RouteUtils to NetdUtils, because
all methods depend on INetd, and some of the targets
do not/cannot depend on netd-client
• Restrict the files used by the filegroup
net-utils-framework-wifi-common-srcs, since that
target does not necessarily provide all dependencies
to all its users.
• Don't move NetworkMonitorUtils, since it depends on
SdkLevel, and net-utils-framework-common-srcs is
using **/*.java. It would have been possible to list
explicitly all files actually necessary in this
filegroup, but NetworkMonitorUtils is actually only
used by the networking modules and not the framework.
Eventually it should move but it doesn't have to
be in this patch, which is complicated enough as
it is.
• RouteUtils is now empty, because some new methods
will be added to it soon and it is less expensive
to keep it empty than to remove it now and add it
again later.
• Merge NetdUtils, and unify the constants.
Some changes to satisfy checkstyle :
• Remove unused imports
• Reorder modifiers in InterfaceController
• Remove {} in IpNetworkMonitor
• Remove redundant public modifier in IpNetworkMonitor
• Add javadoc to a few methods
• Add whitespace around | in InterfaceParams
However, don't rename members in IpNeighborMonitor
like checkstyle would prefer because this would make
migration to this more involved.
Test: NetworkStaticLibsTests NetdStaticLibTests
Change-Id: I439121cba5d7ea95aa4d6c80ea25207c316880a0
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index a7a027c..4fc93ce 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -38,6 +38,7 @@
"device/com/android/net/module/util/FdEventsReader.java",
"device/com/android/net/module/util/HexDump.java",
"device/com/android/net/module/util/PacketReader.java",
+ "device/com/android/net/module/util/SharedLog.java",
// This library is used by system modules, for which the system health impact of Kotlin
// has not yet been evaluated. Annotations may need jarjar'ing.
// "src_devicecommon/**/*.kt",
@@ -95,7 +96,7 @@
java_library {
name: "net-utils-device-common-netlink",
- // TODO: Ipv6Utils and Struct stuff could be separated out of th netlink library into
+ // TODO: Ipv6Utils and Struct stuff could be separated out of the netlink library into
// an individual Struct library, and remove the net-utils-framework-common lib dependency.
// But there is no need doing this at the moment.
srcs: [
@@ -125,6 +126,36 @@
}
java_library {
+ // TODO : this target should probably be folded into net-utils-device-common
+ name: "net-utils-device-common-ip",
+ srcs: [
+ "device/com/android/net/module/util/ip/*.java",
+ ],
+ sdk_version: "module_current",
+ min_sdk_version: "29",
+ visibility: [
+ "//frameworks/libs/net/common/tests:__subpackages__",
+ "//frameworks/libs/net/common/testutils:__subpackages__",
+ "//packages/modules/Connectivity:__subpackages__",
+ "//packages/modules/NetworkStack:__subpackages__",
+ ],
+ libs: [
+ "framework-annotations-lib",
+ "framework-connectivity",
+ ],
+ static_libs: [
+ "net-utils-device-common",
+ "net-utils-device-common-netlink",
+ "net-utils-framework-common",
+ "netd-client",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ "//apex_available:platform",
+ ],
+}
+
+java_library {
name: "net-utils-framework-common",
srcs: [
":net-utils-framework-common-srcs",
@@ -205,7 +236,11 @@
filegroup {
name: "net-utils-framework-wifi-common-srcs",
srcs: [
- "framework/com/android/net/module/util/**/*.java",
+ "framework/com/android/net/module/util/DnsSdTxtRecord.java",
+ "framework/com/android/net/module/util/Inet4AddressUtils.java",
+ "framework/com/android/net/module/util/InetAddressUtils.java",
+ "framework/com/android/net/module/util/MacAddressUtils.java",
+ "framework/com/android/net/module/util/NetUtils.java",
],
path: "framework",
visibility: [
diff --git a/staticlibs/device/com/android/net/module/util/SharedLog.java b/staticlibs/device/com/android/net/module/util/SharedLog.java
new file mode 100644
index 0000000..37c6f6d
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/SharedLog.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2017 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.net.module.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.time.LocalDateTime;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.StringJoiner;
+
+
+/**
+ * Class to centralize logging functionality for tethering.
+ *
+ * All access to class methods other than dump() must be on the same thread.
+ *
+ * @hide
+ */
+public class SharedLog {
+ private static final int DEFAULT_MAX_RECORDS = 500;
+ private static final String COMPONENT_DELIMITER = ".";
+
+ private enum Category {
+ NONE,
+ ERROR,
+ MARK,
+ WARN,
+ }
+
+ private final LocalLog mLocalLog;
+ // The tag to use for output to the system log. This is not output to the
+ // LocalLog because that would be redundant.
+ private final String mTag;
+ // The component (or subcomponent) of a system that is sharing this log.
+ // This can grow in depth if components call forSubComponent() to obtain
+ // their SharedLog instance. The tag is not included in the component for
+ // brevity.
+ private final String mComponent;
+
+ public SharedLog(String tag) {
+ this(DEFAULT_MAX_RECORDS, tag);
+ }
+
+ public SharedLog(int maxRecords, String tag) {
+ this(new LocalLog(maxRecords), tag, tag);
+ }
+
+ private SharedLog(LocalLog localLog, String tag, String component) {
+ mLocalLog = localLog;
+ mTag = tag;
+ mComponent = component;
+ }
+
+ public String getTag() {
+ return mTag;
+ }
+
+ /**
+ * Create a SharedLog based on this log with an additional component prefix on each logged line.
+ */
+ public SharedLog forSubComponent(String component) {
+ if (!isRootLogInstance()) {
+ component = mComponent + COMPONENT_DELIMITER + component;
+ }
+ return new SharedLog(mLocalLog, mTag, component);
+ }
+
+ /**
+ * Dump the contents of this log.
+ *
+ * <p>This method may be called on any thread.
+ */
+ public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ mLocalLog.dump(writer);
+ }
+
+ //////
+ // Methods that both log an entry and emit it to the system log.
+ //////
+
+ /**
+ * Log an error due to an exception. This does not include the exception stacktrace.
+ *
+ * <p>The log entry will be also added to the system log.
+ * @see #e(String, Throwable)
+ */
+ public void e(Exception e) {
+ Log.e(mTag, record(Category.ERROR, e.toString()));
+ }
+
+ /**
+ * Log an error message.
+ *
+ * <p>The log entry will be also added to the system log.
+ */
+ public void e(String msg) {
+ Log.e(mTag, record(Category.ERROR, msg));
+ }
+
+ /**
+ * Log an error due to an exception, with the exception stacktrace if provided.
+ *
+ * <p>The error and exception message appear in the shared log, but the stacktrace is only
+ * logged in general log output (logcat). The log entry will be also added to the system log.
+ */
+ public void e(@NonNull String msg, @Nullable Throwable exception) {
+ if (exception == null) {
+ e(msg);
+ return;
+ }
+ Log.e(mTag, record(Category.ERROR, msg + ": " + exception.getMessage()), exception);
+ }
+
+ /**
+ * Log an informational message.
+ *
+ * <p>The log entry will be also added to the system log.
+ */
+ public void i(String msg) {
+ Log.i(mTag, record(Category.NONE, msg));
+ }
+
+ /**
+ * Log a warning message.
+ *
+ * <p>The log entry will be also added to the system log.
+ */
+ public void w(String msg) {
+ Log.w(mTag, record(Category.WARN, msg));
+ }
+
+ //////
+ // Methods that only log an entry (and do NOT emit to the system log).
+ //////
+
+ /**
+ * Log a general message to be only included in the in-memory log.
+ *
+ * <p>The log entry will *not* be added to the system log.
+ */
+ public void log(String msg) {
+ record(Category.NONE, msg);
+ }
+
+ /**
+ * Log a general, formatted message to be only included in the in-memory log.
+ *
+ * <p>The log entry will *not* be added to the system log.
+ * @see String#format(String, Object...)
+ */
+ public void logf(String fmt, Object... args) {
+ log(String.format(fmt, args));
+ }
+
+ /**
+ * Log a message with MARK level.
+ *
+ * <p>The log entry will *not* be added to the system log.
+ */
+ public void mark(String msg) {
+ record(Category.MARK, msg);
+ }
+
+ private String record(Category category, String msg) {
+ final String entry = logLine(category, msg);
+ mLocalLog.append(entry);
+ return entry;
+ }
+
+ private String logLine(Category category, String msg) {
+ final StringJoiner sj = new StringJoiner(" ");
+ if (!isRootLogInstance()) sj.add("[" + mComponent + "]");
+ if (category != Category.NONE) sj.add(category.toString());
+ return sj.add(msg).toString();
+ }
+
+ // Check whether this SharedLog instance is nominally the top level in
+ // a potential hierarchy of shared logs (the root of a tree),
+ // or is a subcomponent within the hierarchy.
+ private boolean isRootLogInstance() {
+ return TextUtils.isEmpty(mComponent) || mComponent.equals(mTag);
+ }
+
+ private static final class LocalLog {
+ private final Deque<String> mLog;
+ private final int mMaxLines;
+
+ LocalLog(int maxLines) {
+ mMaxLines = Math.max(0, maxLines);
+ mLog = new ArrayDeque<>(mMaxLines);
+ }
+
+ synchronized void append(String logLine) {
+ if (mMaxLines <= 0) return;
+ while (mLog.size() >= mMaxLines) {
+ mLog.remove();
+ }
+ mLog.add(LocalDateTime.now() + " - " + logLine);
+ }
+
+ /**
+ * Dumps the content of local log to print writer with each log entry
+ *
+ * @param pw printer writer to write into
+ */
+ synchronized void dump(PrintWriter pw) {
+ for (final String s : mLog) {
+ pw.println(s);
+ }
+ }
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ip/ConntrackMonitor.java b/staticlibs/device/com/android/net/module/util/ip/ConntrackMonitor.java
new file mode 100644
index 0000000..420a544
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ip/ConntrackMonitor.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2020 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.net.module.util.ip;
+
+import static com.android.net.module.util.netlink.ConntrackMessage.DYING_MASK;
+import static com.android.net.module.util.netlink.ConntrackMessage.ESTABLISHED_MASK;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+import android.system.OsConstants;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.netlink.ConntrackMessage;
+import com.android.net.module.util.netlink.NetlinkConstants;
+import com.android.net.module.util.netlink.NetlinkMessage;
+
+import java.util.Objects;
+
+
+/**
+ * ConntrackMonitor.
+ *
+ * Monitors the netfilter conntrack notifications and presents to callers
+ * ConntrackEvents describing each event.
+ *
+ * @hide
+ */
+public class ConntrackMonitor extends NetlinkMonitor {
+ private static final String TAG = ConntrackMonitor.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ // Reference kernel/uapi/linux/netfilter/nfnetlink_compat.h
+ public static final int NF_NETLINK_CONNTRACK_NEW = 1;
+ public static final int NF_NETLINK_CONNTRACK_UPDATE = 2;
+ public static final int NF_NETLINK_CONNTRACK_DESTROY = 4;
+
+ // The socket receive buffer size in bytes. If too many conntrack messages are sent too
+ // quickly, the conntrack messages can overflow the socket receive buffer. This can happen
+ // if too many connections are disconnected by losing network and so on. Use a large-enough
+ // buffer to avoid the error ENOBUFS while listening to the conntrack messages.
+ private static final int SOCKET_RECV_BUFSIZE = 6 * 1024 * 1024;
+
+ /**
+ * A class for describing parsed netfilter conntrack events.
+ */
+ public static class ConntrackEvent {
+ /**
+ * Conntrack event type.
+ */
+ public final short msgType;
+ /**
+ * Original direction conntrack tuple.
+ */
+ public final ConntrackMessage.Tuple tupleOrig;
+ /**
+ * Reply direction conntrack tuple.
+ */
+ public final ConntrackMessage.Tuple tupleReply;
+ /**
+ * Connection status. A bitmask of ip_conntrack_status enum flags.
+ */
+ public final int status;
+ /**
+ * Conntrack timeout.
+ */
+ public final int timeoutSec;
+
+ public ConntrackEvent(ConntrackMessage msg) {
+ this.msgType = msg.getHeader().nlmsg_type;
+ this.tupleOrig = msg.tupleOrig;
+ this.tupleReply = msg.tupleReply;
+ this.status = msg.status;
+ this.timeoutSec = msg.timeoutSec;
+ }
+
+ @VisibleForTesting
+ public ConntrackEvent(short msgType, ConntrackMessage.Tuple tupleOrig,
+ ConntrackMessage.Tuple tupleReply, int status, int timeoutSec) {
+ this.msgType = msgType;
+ this.tupleOrig = tupleOrig;
+ this.tupleReply = tupleReply;
+ this.status = status;
+ this.timeoutSec = timeoutSec;
+ }
+
+ @Override
+ @VisibleForTesting
+ public boolean equals(Object o) {
+ if (!(o instanceof ConntrackEvent)) return false;
+ ConntrackEvent that = (ConntrackEvent) o;
+ return this.msgType == that.msgType
+ && Objects.equals(this.tupleOrig, that.tupleOrig)
+ && Objects.equals(this.tupleReply, that.tupleReply)
+ && this.status == that.status
+ && this.timeoutSec == that.timeoutSec;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(msgType, tupleOrig, tupleReply, status, timeoutSec);
+ }
+
+ @Override
+ public String toString() {
+ return "ConntrackEvent{"
+ + "msg_type{"
+ + NetlinkConstants.stringForNlMsgType(msgType, OsConstants.NETLINK_NETFILTER)
+ + "}, "
+ + "tuple_orig{" + tupleOrig + "}, "
+ + "tuple_reply{" + tupleReply + "}, "
+ + "status{"
+ + status + "(" + ConntrackMessage.stringForIpConntrackStatus(status) + ")"
+ + "}, "
+ + "timeout_sec{" + Integer.toUnsignedLong(timeoutSec) + "}"
+ + "}";
+ }
+
+ /**
+ * Check the established NAT session conntrack message.
+ *
+ * @param msg the conntrack message to check.
+ * @return true if an established NAT message, false if not.
+ */
+ public static boolean isEstablishedNatSession(@NonNull ConntrackMessage msg) {
+ if (msg.getMessageType() != NetlinkConstants.IPCTNL_MSG_CT_NEW) return false;
+ if (msg.tupleOrig == null) return false;
+ if (msg.tupleReply == null) return false;
+ if (msg.timeoutSec == 0) return false;
+ if ((msg.status & ESTABLISHED_MASK) != ESTABLISHED_MASK) return false;
+
+ return true;
+ }
+
+ /**
+ * Check the dying NAT session conntrack message.
+ * Note that IPCTNL_MSG_CT_DELETE event has no CTA_TIMEOUT attribute.
+ *
+ * @param msg the conntrack message to check.
+ * @return true if a dying NAT message, false if not.
+ */
+ public static boolean isDyingNatSession(@NonNull ConntrackMessage msg) {
+ if (msg.getMessageType() != NetlinkConstants.IPCTNL_MSG_CT_DELETE) return false;
+ if (msg.tupleOrig == null) return false;
+ if (msg.tupleReply == null) return false;
+ if (msg.timeoutSec != 0) return false;
+ if ((msg.status & DYING_MASK) != DYING_MASK) return false;
+
+ return true;
+ }
+ }
+
+ /**
+ * A callback to caller for conntrack event.
+ */
+ public interface ConntrackEventConsumer {
+ /**
+ * Every conntrack event received on the netlink socket is passed in
+ * here.
+ */
+ void accept(@NonNull ConntrackEvent event);
+ }
+
+ private final ConntrackEventConsumer mConsumer;
+
+ public ConntrackMonitor(@NonNull Handler h, @NonNull SharedLog log,
+ @NonNull ConntrackEventConsumer cb) {
+ super(h, log, TAG, OsConstants.NETLINK_NETFILTER, NF_NETLINK_CONNTRACK_NEW
+ | NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY, SOCKET_RECV_BUFSIZE);
+ mConsumer = cb;
+ }
+
+ @Override
+ public void processNetlinkMessage(NetlinkMessage nlMsg, final long whenMs) {
+ if (!(nlMsg instanceof ConntrackMessage)) {
+ mLog.e("non-conntrack msg: " + nlMsg);
+ return;
+ }
+
+ final ConntrackMessage conntrackMsg = (ConntrackMessage) nlMsg;
+ if (!(ConntrackEvent.isEstablishedNatSession(conntrackMsg)
+ || ConntrackEvent.isDyingNatSession(conntrackMsg))) {
+ return;
+ }
+
+ mConsumer.accept(new ConntrackEvent(conntrackMsg));
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ip/InterfaceController.java b/staticlibs/device/com/android/net/module/util/ip/InterfaceController.java
new file mode 100644
index 0000000..7277fec
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ip/InterfaceController.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.net.module.util.ip;
+
+import static android.net.INetd.IF_STATE_DOWN;
+import static android.net.INetd.IF_STATE_UP;
+
+import android.net.INetd;
+import android.net.InterfaceConfigurationParcel;
+import android.net.LinkAddress;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.system.OsConstants;
+
+import com.android.net.module.util.SharedLog;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+
+/**
+ * Encapsulates the multiple IP configuration operations performed on an interface.
+ *
+ * TODO: refactor/eliminate the redundant ways to set and clear addresses.
+ *
+ * @hide
+ */
+public class InterfaceController {
+ private static final boolean DBG = false;
+
+ private final String mIfName;
+ private final INetd mNetd;
+ private final SharedLog mLog;
+
+ public InterfaceController(String ifname, INetd netd, SharedLog log) {
+ mIfName = ifname;
+ mNetd = netd;
+ mLog = log;
+ }
+
+ /**
+ * Set the IPv4 address and also optionally bring the interface up or down.
+ */
+ public boolean setInterfaceConfiguration(final LinkAddress ipv4Addr,
+ final Boolean setIfaceUp) {
+ if (!(ipv4Addr.getAddress() instanceof Inet4Address)) {
+ throw new IllegalArgumentException("Invalid or mismatched Inet4Address");
+ }
+ // Note: currently netd only support INetd#IF_STATE_UP and #IF_STATE_DOWN.
+ // Other flags would be ignored.
+
+ final InterfaceConfigurationParcel ifConfig = new InterfaceConfigurationParcel();
+ ifConfig.ifName = mIfName;
+ ifConfig.ipv4Addr = ipv4Addr.getAddress().getHostAddress();
+ ifConfig.prefixLength = ipv4Addr.getPrefixLength();
+ // Netd ignores hwaddr in interfaceSetCfg.
+ ifConfig.hwAddr = "";
+ if (setIfaceUp == null) {
+ // Empty array means no change.
+ ifConfig.flags = new String[0];
+ } else {
+ // Netd ignores any flag that's not IF_STATE_UP or IF_STATE_DOWN in interfaceSetCfg.
+ ifConfig.flags = setIfaceUp.booleanValue()
+ ? new String[] {IF_STATE_UP} : new String[] {IF_STATE_DOWN};
+ }
+ try {
+ mNetd.interfaceSetCfg(ifConfig);
+ } catch (RemoteException | ServiceSpecificException e) {
+ logError("Setting IPv4 address to %s/%d failed: %s",
+ ifConfig.ipv4Addr, ifConfig.prefixLength, e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Set the IPv4 address of the interface.
+ */
+ public boolean setIPv4Address(final LinkAddress address) {
+ return setInterfaceConfiguration(address, null);
+ }
+
+ /**
+ * Clear the IPv4Address of the interface.
+ */
+ public boolean clearIPv4Address() {
+ return setIPv4Address(new LinkAddress("0.0.0.0/0"));
+ }
+
+ private boolean setEnableIPv6(boolean enabled) {
+ try {
+ mNetd.interfaceSetEnableIPv6(mIfName, enabled);
+ } catch (RemoteException | ServiceSpecificException e) {
+ logError("%s IPv6 failed: %s", (enabled ? "enabling" : "disabling"), e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Enable IPv6 on the interface.
+ */
+ public boolean enableIPv6() {
+ return setEnableIPv6(true);
+ }
+
+ /**
+ * Disable IPv6 on the interface.
+ */
+ public boolean disableIPv6() {
+ return setEnableIPv6(false);
+ }
+
+ /**
+ * Enable or disable IPv6 privacy extensions on the interface.
+ * @param enabled Whether the extensions should be enabled.
+ */
+ public boolean setIPv6PrivacyExtensions(boolean enabled) {
+ try {
+ mNetd.interfaceSetIPv6PrivacyExtensions(mIfName, enabled);
+ } catch (RemoteException | ServiceSpecificException e) {
+ logError("error %s IPv6 privacy extensions: %s",
+ (enabled ? "enabling" : "disabling"), e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Set IPv6 address generation mode on the interface.
+ *
+ * <p>IPv6 should be disabled before changing the mode.
+ */
+ public boolean setIPv6AddrGenModeIfSupported(int mode) {
+ try {
+ mNetd.setIPv6AddrGenMode(mIfName, mode);
+ } catch (RemoteException e) {
+ logError("Unable to set IPv6 addrgen mode: %s", e);
+ return false;
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode != OsConstants.EOPNOTSUPP) {
+ logError("Unable to set IPv6 addrgen mode: %s", e);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Add an address to the interface.
+ */
+ public boolean addAddress(LinkAddress addr) {
+ return addAddress(addr.getAddress(), addr.getPrefixLength());
+ }
+
+ /**
+ * Add an address to the interface.
+ */
+ public boolean addAddress(InetAddress ip, int prefixLen) {
+ try {
+ mNetd.interfaceAddAddress(mIfName, ip.getHostAddress(), prefixLen);
+ } catch (ServiceSpecificException | RemoteException e) {
+ logError("failed to add %s/%d: %s", ip, prefixLen, e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Remove an address from the interface.
+ */
+ public boolean removeAddress(InetAddress ip, int prefixLen) {
+ try {
+ mNetd.interfaceDelAddress(mIfName, ip.getHostAddress(), prefixLen);
+ } catch (ServiceSpecificException | RemoteException e) {
+ logError("failed to remove %s/%d: %s", ip, prefixLen, e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Remove all addresses from the interface.
+ */
+ public boolean clearAllAddresses() {
+ try {
+ mNetd.interfaceClearAddrs(mIfName);
+ } catch (Exception e) {
+ logError("Failed to clear addresses: %s", e);
+ return false;
+ }
+ return true;
+ }
+
+ private void logError(String fmt, Object... args) {
+ mLog.e(String.format(fmt, args));
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ip/IpNeighborMonitor.java b/staticlibs/device/com/android/net/module/util/ip/IpNeighborMonitor.java
new file mode 100644
index 0000000..88f8c9d
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ip/IpNeighborMonitor.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2017 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.net.module.util.ip;
+
+import static android.system.OsConstants.NETLINK_ROUTE;
+
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELNEIGH;
+import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+import static com.android.net.module.util.netlink.NetlinkConstants.stringForNlMsgType;
+
+import android.net.MacAddress;
+import android.os.Handler;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.NetlinkSocket;
+import com.android.net.module.util.netlink.RtNetlinkNeighborMessage;
+import com.android.net.module.util.netlink.StructNdMsg;
+
+import java.net.InetAddress;
+import java.util.StringJoiner;
+
+/**
+ * IpNeighborMonitor.
+ *
+ * Monitors the kernel rtnetlink neighbor notifications and presents to callers
+ * NeighborEvents describing each event. Callers can provide a consumer instance
+ * to both filter (e.g. by interface index and IP address) and handle the
+ * generated NeighborEvents.
+ *
+ * @hide
+ */
+public class IpNeighborMonitor extends NetlinkMonitor {
+ private static final String TAG = IpNeighborMonitor.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ /**
+ * Make the kernel perform neighbor reachability detection (IPv4 ARP or IPv6 ND)
+ * for the given IP address on the specified interface index.
+ *
+ * @return 0 if the request was successfully passed to the kernel; otherwise return
+ * a non-zero error code.
+ */
+ public static int startKernelNeighborProbe(int ifIndex, InetAddress ip) {
+ final String msgSnippet = "probing ip=" + ip.getHostAddress() + "%" + ifIndex;
+ if (DBG) Log.d(TAG, msgSnippet);
+
+ final byte[] msg = RtNetlinkNeighborMessage.newNewNeighborMessage(
+ 1, ip, StructNdMsg.NUD_PROBE, ifIndex, null);
+
+ try {
+ NetlinkSocket.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Error " + msgSnippet + ": " + e);
+ return -e.errno;
+ }
+
+ return 0;
+ }
+
+ /**
+ * An event about a neighbor.
+ */
+ public static class NeighborEvent {
+ final long elapsedMs;
+ final short msgType;
+ final int ifindex;
+ final InetAddress ip;
+ final short nudState;
+ final MacAddress macAddr;
+
+ public NeighborEvent(long elapsedMs, short msgType, int ifindex, InetAddress ip,
+ short nudState, MacAddress macAddr) {
+ this.elapsedMs = elapsedMs;
+ this.msgType = msgType;
+ this.ifindex = ifindex;
+ this.ip = ip;
+ this.nudState = nudState;
+ this.macAddr = macAddr;
+ }
+
+ boolean isConnected() {
+ return (msgType != RTM_DELNEIGH) && StructNdMsg.isNudStateConnected(nudState);
+ }
+
+ boolean isValid() {
+ return (msgType != RTM_DELNEIGH) && StructNdMsg.isNudStateValid(nudState);
+ }
+
+ @Override
+ public String toString() {
+ final StringJoiner j = new StringJoiner(",", "NeighborEvent{", "}");
+ return j.add("@" + elapsedMs)
+ .add(stringForNlMsgType(msgType, NETLINK_ROUTE))
+ .add("if=" + ifindex)
+ .add(ip.getHostAddress())
+ .add(StructNdMsg.stringForNudState(nudState))
+ .add("[" + macAddr + "]")
+ .toString();
+ }
+ }
+
+ /**
+ * A client that consumes NeighborEvent instances.
+ * Implement this to listen to neighbor events.
+ */
+ public interface NeighborEventConsumer {
+ // Every neighbor event received on the netlink socket is passed in
+ // here. Subclasses should filter for events of interest.
+ /**
+ * Consume a neighbor event
+ * @param event the event
+ */
+ void accept(NeighborEvent event);
+ }
+
+ private final NeighborEventConsumer mConsumer;
+
+ public IpNeighborMonitor(Handler h, SharedLog log, NeighborEventConsumer cb) {
+ super(h, log, TAG, NETLINK_ROUTE, OsConstants.RTMGRP_NEIGH);
+ mConsumer = (cb != null) ? cb : (event) -> { /* discard */ };
+ }
+
+ @Override
+ public void processNetlinkMessage(NetlinkMessage nlMsg, final long whenMs) {
+ if (!(nlMsg instanceof RtNetlinkNeighborMessage)) {
+ mLog.e("non-rtnetlink neighbor msg: " + nlMsg);
+ return;
+ }
+
+ final RtNetlinkNeighborMessage neighMsg = (RtNetlinkNeighborMessage) nlMsg;
+ final short msgType = neighMsg.getHeader().nlmsg_type;
+ final StructNdMsg ndMsg = neighMsg.getNdHeader();
+ if (ndMsg == null) {
+ mLog.e("RtNetlinkNeighborMessage without ND message header!");
+ return;
+ }
+
+ final int ifindex = ndMsg.ndm_ifindex;
+ final InetAddress destination = neighMsg.getDestination();
+ final short nudState =
+ (msgType == RTM_DELNEIGH)
+ ? StructNdMsg.NUD_NONE
+ : ndMsg.ndm_state;
+
+ final NeighborEvent event = new NeighborEvent(
+ whenMs, msgType, ifindex, destination, nudState,
+ getMacAddress(neighMsg.getLinkLayerAddress()));
+
+ if (VDBG) {
+ Log.d(TAG, neighMsg.toString());
+ }
+ if (DBG) {
+ Log.d(TAG, event.toString());
+ }
+
+ mConsumer.accept(event);
+ }
+
+ private static MacAddress getMacAddress(byte[] linkLayerAddress) {
+ if (linkLayerAddress != null) {
+ try {
+ return MacAddress.fromBytes(linkLayerAddress);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Failed to parse link-layer address: " + hexify(linkLayerAddress));
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java b/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java
new file mode 100644
index 0000000..8589876
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2020 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.net.module.util.ip;
+
+import static android.net.util.SocketUtils.makeNetlinkSocketAddress;
+import static android.system.OsConstants.AF_NETLINK;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_NONBLOCK;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_RCVBUF;
+
+import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+
+import android.annotation.NonNull;
+import android.net.util.SocketUtils;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import com.android.net.module.util.PacketReader;
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.netlink.NetlinkErrorMessage;
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.NetlinkSocket;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * A simple base class to listen for netlink broadcasts.
+ *
+ * Opens a netlink socket of the given family and binds to the specified groups. Polls the socket
+ * from the event loop of the passed-in {@link Handler}, and calls the subclass-defined
+ * {@link #processNetlinkMessage} method on the handler thread for each netlink message that
+ * arrives. Currently ignores all netlink errors.
+ * @hide
+ */
+public class NetlinkMonitor extends PacketReader {
+ protected final SharedLog mLog;
+ protected final String mTag;
+ private final int mFamily;
+ private final int mBindGroups;
+ private final int mSockRcvbufSize;
+
+ private static final boolean DBG = false;
+
+ // Default socket receive buffer size. This means the specific buffer size is not set.
+ private static final int DEFAULT_SOCKET_RECV_BUFSIZE = -1;
+
+ /**
+ * Constructs a new {@code NetlinkMonitor} instance.
+ *
+ * @param h The Handler on which to poll for messages and on which to call
+ * {@link #processNetlinkMessage}.
+ * @param log A SharedLog to log to.
+ * @param tag The log tag to use for log messages.
+ * @param family the Netlink socket family to, e.g., {@code NETLINK_ROUTE}.
+ * @param bindGroups the netlink groups to bind to.
+ * @param sockRcvbufSize the specific socket receive buffer size in bytes. -1 means that don't
+ * set the specific socket receive buffer size in #createFd and use the default value in
+ * /proc/sys/net/core/rmem_default file. See SO_RCVBUF in man-pages/socket.
+ */
+ public NetlinkMonitor(@NonNull Handler h, @NonNull SharedLog log, @NonNull String tag,
+ int family, int bindGroups, int sockRcvbufSize) {
+ super(h, NetlinkSocket.DEFAULT_RECV_BUFSIZE);
+ mLog = log.forSubComponent(tag);
+ mTag = tag;
+ mFamily = family;
+ mBindGroups = bindGroups;
+ mSockRcvbufSize = sockRcvbufSize;
+ }
+
+ public NetlinkMonitor(@NonNull Handler h, @NonNull SharedLog log, @NonNull String tag,
+ int family, int bindGroups) {
+ this(h, log, tag, family, bindGroups, DEFAULT_SOCKET_RECV_BUFSIZE);
+ }
+
+ @Override
+ protected FileDescriptor createFd() {
+ FileDescriptor fd = null;
+
+ try {
+ fd = Os.socket(AF_NETLINK, SOCK_DGRAM | SOCK_NONBLOCK, mFamily);
+ if (mSockRcvbufSize != DEFAULT_SOCKET_RECV_BUFSIZE) {
+ Os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, mSockRcvbufSize);
+ }
+ Os.bind(fd, makeNetlinkSocketAddress(0, mBindGroups));
+ NetlinkSocket.connectToKernel(fd);
+
+ if (DBG) {
+ final SocketAddress nlAddr = Os.getsockname(fd);
+ Log.d(mTag, "bound to sockaddr_nl{" + nlAddr.toString() + "}");
+ }
+ } catch (ErrnoException | SocketException e) {
+ logError("Failed to create rtnetlink socket", e);
+ closeSocketQuietly(fd);
+ return null;
+ }
+
+ return fd;
+ }
+
+ @Override
+ protected void handlePacket(byte[] recvbuf, int length) {
+ final long whenMs = SystemClock.elapsedRealtime();
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(recvbuf, 0, length);
+ byteBuffer.order(ByteOrder.nativeOrder());
+
+ while (byteBuffer.remaining() > 0) {
+ try {
+ final int position = byteBuffer.position();
+ final NetlinkMessage nlMsg = NetlinkMessage.parse(byteBuffer, mFamily);
+ if (nlMsg == null || nlMsg.getHeader() == null) {
+ byteBuffer.position(position);
+ mLog.e("unparsable netlink msg: " + hexify(byteBuffer));
+ break;
+ }
+
+ if (nlMsg instanceof NetlinkErrorMessage) {
+ mLog.e("netlink error: " + nlMsg);
+ continue;
+ }
+
+ processNetlinkMessage(nlMsg, whenMs);
+ } catch (Exception e) {
+ mLog.e("Error handling netlink message", e);
+ }
+ }
+ }
+
+ // TODO: move NetworkStackUtils to frameworks/libs/net for NetworkStackUtils#closeSocketQuietly.
+ private void closeSocketQuietly(FileDescriptor fd) {
+ try {
+ SocketUtils.closeSocket(fd);
+ } catch (IOException ignored) {
+ }
+ }
+
+ /**
+ * Processes one netlink message. Must be overridden by subclasses.
+ * @param nlMsg the message to process.
+ * @param whenMs the timestamp, as measured by {@link SystemClock#elapsedRealtime}, when the
+ * message was received.
+ */
+ protected void processNetlinkMessage(NetlinkMessage nlMsg, long whenMs) { }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/InterfaceParams.java b/staticlibs/framework/com/android/net/module/util/InterfaceParams.java
new file mode 100644
index 0000000..30762eb
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/InterfaceParams.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.net.module.util;
+
+import android.net.MacAddress;
+import android.text.TextUtils;
+
+import java.net.NetworkInterface;
+import java.net.SocketException;
+
+
+/**
+ * Encapsulate the interface parameters common to IpClient/IpServer components.
+ *
+ * Basically all java.net.NetworkInterface methods throw Exceptions. IpClient
+ * and IpServer (sub)components need most or all of this information at some
+ * point during their lifecycles, so pass only this simplified object around
+ * which can be created once when IpClient/IpServer are told to start.
+ *
+ * @hide
+ */
+public class InterfaceParams {
+ public final String name;
+ public final int index;
+ public final boolean hasMacAddress;
+ public final MacAddress macAddr;
+ public final int defaultMtu;
+
+ // TODO: move the below to NetworkStackConstants when this class is moved to the NetworkStack.
+ private static final int ETHER_MTU = 1500;
+ private static final int IPV6_MIN_MTU = 1280;
+
+
+ /**
+ * Return InterfaceParams corresponding with an interface name
+ * @param name the interface name
+ */
+ public static InterfaceParams getByName(String name) {
+ final NetworkInterface netif = getNetworkInterfaceByName(name);
+ if (netif == null) return null;
+
+ // Not all interfaces have MAC addresses, e.g. rmnet_data0.
+ final MacAddress macAddr = getMacAddress(netif);
+
+ try {
+ return new InterfaceParams(name, netif.getIndex(), macAddr, netif.getMTU());
+ } catch (IllegalArgumentException | SocketException e) {
+ return null;
+ }
+ }
+
+ public InterfaceParams(String name, int index, MacAddress macAddr) {
+ this(name, index, macAddr, ETHER_MTU);
+ }
+
+ public InterfaceParams(String name, int index, MacAddress macAddr, int defaultMtu) {
+ if (TextUtils.isEmpty(name)) {
+ throw new IllegalArgumentException("impossible interface name");
+ }
+
+ if (index <= 0) throw new IllegalArgumentException("invalid interface index");
+
+ this.name = name;
+ this.index = index;
+ this.hasMacAddress = (macAddr != null);
+ this.macAddr = hasMacAddress ? macAddr : MacAddress.fromBytes(new byte[] {
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 });
+ this.defaultMtu = (defaultMtu > IPV6_MIN_MTU) ? defaultMtu : IPV6_MIN_MTU;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s/%d/%s/%d", name, index, macAddr, defaultMtu);
+ }
+
+ private static NetworkInterface getNetworkInterfaceByName(String name) {
+ try {
+ return NetworkInterface.getByName(name);
+ } catch (NullPointerException | SocketException e) {
+ return null;
+ }
+ }
+
+ private static MacAddress getMacAddress(NetworkInterface netif) {
+ try {
+ return MacAddress.fromBytes(netif.getHardwareAddress());
+ } catch (IllegalArgumentException | NullPointerException | SocketException e) {
+ return null;
+ }
+ }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/RouteUtils.java b/staticlibs/framework/com/android/net/module/util/RouteUtils.java
new file mode 100644
index 0000000..c241680
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/RouteUtils.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.net.module.util;
+
+/** @hide */
+// RouteUtils is now empty, because some new methods will be added to it soon and it is less
+// expensive to keep it empty than to remove it now and add it again later.
+public class RouteUtils {
+}
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 23835fe..a9f9d70 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -13,10 +13,12 @@
defaults: ["framework-connectivity-test-defaults"],
static_libs: [
"net-utils-framework-common",
+ "net-utils-device-common-ip",
"androidx.test.rules",
"mockito-target-extended-minus-junit4",
"net-utils-device-common",
"net-tests-utils",
+ "netd-client",
],
libs: [
"android.test.runner",
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java
new file mode 100644
index 0000000..446e881
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2017 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.net.module.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SharedLogTest {
+ private static final String TIMESTAMP_PATTERN = "\\d{2}:\\d{2}:\\d{2}";
+ private static final String TIMESTAMP = "HH:MM:SS";
+
+ @Test
+ public void testBasicOperation() {
+ final SharedLog logTop = new SharedLog("top");
+ logTop.mark("first post!");
+
+ final SharedLog logLevel2a = logTop.forSubComponent("twoA");
+ final SharedLog logLevel2b = logTop.forSubComponent("twoB");
+ logLevel2b.e("2b or not 2b");
+ logLevel2b.e("No exception", null);
+ logLevel2b.e("Wait, here's one", new Exception("Test"));
+ logLevel2a.w("second post?");
+
+ final SharedLog logLevel3 = logLevel2a.forSubComponent("three");
+ logTop.log("still logging");
+ logLevel3.log("3 >> 2");
+ logLevel2a.mark("ok: last post");
+
+ final String[] expected = {
+ " - MARK first post!",
+ " - [twoB] ERROR 2b or not 2b",
+ " - [twoB] ERROR No exception",
+ // No stacktrace in shared log, only in logcat
+ " - [twoB] ERROR Wait, here's one: Test",
+ " - [twoA] WARN second post?",
+ " - still logging",
+ " - [twoA.three] 3 >> 2",
+ " - [twoA] MARK ok: last post",
+ };
+ // Verify the logs are all there and in the correct order.
+ verifyLogLines(expected, logTop);
+
+ // In fact, because they all share the same underlying LocalLog,
+ // every subcomponent SharedLog's dump() is identical.
+ verifyLogLines(expected, logLevel2a);
+ verifyLogLines(expected, logLevel2b);
+ verifyLogLines(expected, logLevel3);
+ }
+
+ private static void verifyLogLines(String[] expected, SharedLog log) {
+ final ByteArrayOutputStream ostream = new ByteArrayOutputStream();
+ final PrintWriter pw = new PrintWriter(ostream, true);
+ log.dump(null, pw, null);
+
+ final String dumpOutput = ostream.toString();
+ assertTrue(dumpOutput != null);
+ assertTrue(!"".equals(dumpOutput));
+
+ final String[] lines = dumpOutput.split("\n");
+ assertEquals(expected.length, lines.length);
+
+ for (int i = 0; i < expected.length; i++) {
+ String got = lines[i];
+ String want = expected[i];
+ assertTrue(String.format("'%s' did not contain '%s'", got, want), got.endsWith(want));
+ assertTrue(String.format("'%s' did not contain a %s timestamp", got, TIMESTAMP),
+ got.replaceFirst(TIMESTAMP_PATTERN, TIMESTAMP).contains(TIMESTAMP));
+ }
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/ip/ConntrackMonitorTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/ip/ConntrackMonitorTest.java
new file mode 100644
index 0000000..99d8f07
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/ip/ConntrackMonitorTest.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2020 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.net.module.util.ip;
+
+import static android.system.OsConstants.AF_UNIX;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.SOCK_DGRAM;
+
+import static com.android.net.module.util.netlink.ConntrackMessage.Tuple;
+import static com.android.net.module.util.netlink.ConntrackMessage.TupleIpv4;
+import static com.android.net.module.util.netlink.ConntrackMessage.TupleProto;
+import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_DELETE;
+import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_NEW;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.net.InetAddresses;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.ip.ConntrackMonitor.ConntrackEvent;
+import com.android.net.module.util.netlink.NetlinkConstants;
+import com.android.net.module.util.netlink.NetlinkSocket;
+
+import libcore.util.HexEncoding;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.FileDescriptor;
+import java.io.InterruptedIOException;
+import java.net.Inet4Address;
+
+/**
+ * Tests for ConntrackMonitor.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ConntrackMonitorTest {
+ private static final long TIMEOUT_MS = 10_000L;
+
+ @Mock private SharedLog mLog;
+ @Mock private ConntrackMonitor.ConntrackEventConsumer mConsumer;
+
+ private final HandlerThread mHandlerThread = new HandlerThread(
+ ConntrackMonitorTest.class.getSimpleName());
+
+ // Late init since the handler thread has been started.
+ private Handler mHandler;
+ private TestConntrackMonitor mConntrackMonitor;
+
+ // A version of [ConntrackMonitor] that reads packets from the socket pair, and instead
+ // allows the test to write test packets to the socket pair via [sendMessage].
+ private class TestConntrackMonitor extends ConntrackMonitor {
+ TestConntrackMonitor(@NonNull Handler h, @NonNull SharedLog log,
+ @NonNull ConntrackEventConsumer cb) {
+ super(h, log, cb);
+
+ mReadFd = new FileDescriptor();
+ mWriteFd = new FileDescriptor();
+ try {
+ Os.socketpair(AF_UNIX, SOCK_DGRAM, 0, mWriteFd, mReadFd);
+ } catch (ErrnoException e) {
+ fail("Could not create socket pair: " + e);
+ }
+ }
+
+ @Override
+ protected FileDescriptor createFd() {
+ return mReadFd;
+ }
+
+ private void sendMessage(byte[] msg) {
+ mHandler.post(() -> {
+ try {
+ NetlinkSocket.sendMessage(mWriteFd, msg, 0 /* offset */, msg.length,
+ TIMEOUT_MS);
+ } catch (ErrnoException | InterruptedIOException e) {
+ fail("Unable to send netfilter message: " + e);
+ }
+ });
+ }
+
+ private final FileDescriptor mReadFd;
+ private final FileDescriptor mWriteFd;
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+
+ // ConntrackMonitor needs to be started from the handler thread.
+ final ConditionVariable initDone = new ConditionVariable();
+ mHandler.post(() -> {
+ TestConntrackMonitor m = new TestConntrackMonitor(mHandler, mLog, mConsumer);
+ m.start();
+ mConntrackMonitor = m;
+
+ initDone.open();
+ });
+ if (!initDone.block(TIMEOUT_MS)) {
+ fail("... init monitor timed-out after " + TIMEOUT_MS + "ms");
+ }
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mHandlerThread.quitSafely();
+ }
+
+ public static final String CT_V4NEW_TCP_HEX =
+ // CHECKSTYLE:OFF IndentationCheck
+ // struct nlmsghdr
+ "8C000000" + // length = 140
+ "0001" + // type = NFNL_SUBSYS_CTNETLINK (1) << 8 | IPCTNL_MSG_CT_NEW (0)
+ "0006" + // flags = NLM_F_CREATE (1 << 10) | NLM_F_EXCL (1 << 9)
+ "00000000" + // seqno = 0
+ "00000000" + // pid = 0
+ // struct nfgenmsg
+ "02" + // nfgen_family = AF_INET
+ "00" + // version = NFNETLINK_V0
+ "1234" + // res_id = 0x1234 (big endian)
+ // struct nlattr
+ "3400" + // nla_len = 52
+ "0180" + // nla_type = nested CTA_TUPLE_ORIG
+ // struct nlattr
+ "1400" + // nla_len = 20
+ "0180" + // nla_type = nested CTA_TUPLE_IP
+ "0800 0100 C0A8500C" + // nla_type=CTA_IP_V4_SRC, ip=192.168.80.12
+ "0800 0200 8C700874" + // nla_type=CTA_IP_V4_DST, ip=140.112.8.116
+ // struct nlattr
+ "1C00" + // nla_len = 28
+ "0280" + // nla_type = nested CTA_TUPLE_PROTO
+ "0500 0100 06 000000" + // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+ "0600 0200 F3F1 0000" + // nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)
+ "0600 0300 01BB 0000" + // nla_type=CTA_PROTO_DST_PORT, port=443 (big endian)
+ // struct nlattr
+ "3400" + // nla_len = 52
+ "0280" + // nla_type = nested CTA_TUPLE_REPLY
+ // struct nlattr
+ "1400" + // nla_len = 20
+ "0180" + // nla_type = nested CTA_TUPLE_IP
+ "0800 0100 8C700874" + // nla_type=CTA_IP_V4_SRC, ip=140.112.8.116
+ "0800 0200 6451B301" + // nla_type=CTA_IP_V4_DST, ip=100.81.179.1
+ // struct nlattr
+ "1C00" + // nla_len = 28
+ "0280" + // nla_type = nested CTA_TUPLE_PROTO
+ "0500 0100 06 000000" + // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+ "0600 0200 01BB 0000" + // nla_type=CTA_PROTO_SRC_PORT, port=443 (big endian)
+ "0600 0300 F3F1 0000" + // nla_type=CTA_PROTO_DST_PORT, port=62449 (big endian)
+ // struct nlattr
+ "0800" + // nla_len = 8
+ "0300" + // nla_type = CTA_STATUS
+ "0000019e" + // nla_value = 0b110011110 (big endian)
+ // IPS_SEEN_REPLY (1 << 1) | IPS_ASSURED (1 << 2) |
+ // IPS_CONFIRMED (1 << 3) | IPS_SRC_NAT (1 << 4) |
+ // IPS_SRC_NAT_DONE (1 << 7) | IPS_DST_NAT_DONE (1 << 8)
+ // struct nlattr
+ "0800" + // nla_len = 8
+ "0700" + // nla_type = CTA_TIMEOUT
+ "00000078"; // nla_value = 120 (big endian)
+ // CHECKSTYLE:ON IndentationCheck
+ public static final byte[] CT_V4NEW_TCP_BYTES =
+ HexEncoding.decode(CT_V4NEW_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
+
+ @NonNull
+ private ConntrackEvent makeTestConntrackEvent(short msgType, int status, int timeoutSec) {
+ final Inet4Address privateIp =
+ (Inet4Address) InetAddresses.parseNumericAddress("192.168.80.12");
+ final Inet4Address remoteIp =
+ (Inet4Address) InetAddresses.parseNumericAddress("140.112.8.116");
+ final Inet4Address publicIp =
+ (Inet4Address) InetAddresses.parseNumericAddress("100.81.179.1");
+
+ return new ConntrackEvent(
+ (short) (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8 | msgType),
+ new Tuple(new TupleIpv4(privateIp, remoteIp),
+ new TupleProto((byte) IPPROTO_TCP, (short) 62449, (short) 443)),
+ new Tuple(new TupleIpv4(remoteIp, publicIp),
+ new TupleProto((byte) IPPROTO_TCP, (short) 443, (short) 62449)),
+ status,
+ timeoutSec);
+ }
+
+ @Test
+ public void testConntrackEventNew() throws Exception {
+ final ConntrackEvent expectedEvent = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW,
+ 0x19e /* status */, 120 /* timeoutSec */);
+ mConntrackMonitor.sendMessage(CT_V4NEW_TCP_BYTES);
+ verify(mConsumer, timeout(TIMEOUT_MS)).accept(eq(expectedEvent));
+ }
+
+ @Test
+ public void testConntrackEventEquals() {
+ final ConntrackEvent event1 = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
+ 5678 /* timeoutSec*/);
+ final ConntrackEvent event2 = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
+ 5678 /* timeoutSec*/);
+ assertEquals(event1, event2);
+ }
+
+ @Test
+ public void testConntrackEventNotEquals() {
+ final ConntrackEvent e = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
+ 5678 /* timeoutSec*/);
+
+ final ConntrackEvent typeNotEqual = new ConntrackEvent((short) (e.msgType + 1) /* diff */,
+ e.tupleOrig, e.tupleReply, e.status, e.timeoutSec);
+ assertNotEquals(e, typeNotEqual);
+
+ final ConntrackEvent tupleOrigNotEqual = new ConntrackEvent(e.msgType,
+ null /* diff */, e.tupleReply, e.status, e.timeoutSec);
+ assertNotEquals(e, tupleOrigNotEqual);
+
+ final ConntrackEvent tupleReplyNotEqual = new ConntrackEvent(e.msgType,
+ e.tupleOrig, null /* diff */, e.status, e.timeoutSec);
+ assertNotEquals(e, tupleReplyNotEqual);
+
+ final ConntrackEvent statusNotEqual = new ConntrackEvent(e.msgType,
+ e.tupleOrig, e.tupleReply, e.status + 1 /* diff */, e.timeoutSec);
+ assertNotEquals(e, statusNotEqual);
+
+ final ConntrackEvent timeoutSecNotEqual = new ConntrackEvent(e.msgType,
+ e.tupleOrig, e.tupleReply, e.status, e.timeoutSec + 1 /* diff */);
+ assertNotEquals(e, timeoutSecNotEqual);
+ }
+
+ @Test
+ public void testToString() {
+ final ConntrackEvent event = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW,
+ 0x198 /* status */, 120 /* timeoutSec */);
+ final String expected = ""
+ + "ConntrackEvent{"
+ + "msg_type{IPCTNL_MSG_CT_NEW}, "
+ + "tuple_orig{Tuple{IPPROTO_TCP: 192.168.80.12:62449 -> 140.112.8.116:443}}, "
+ + "tuple_reply{Tuple{IPPROTO_TCP: 140.112.8.116:443 -> 100.81.179.1:62449}}, "
+ + "status{408(IPS_CONFIRMED|IPS_SRC_NAT|IPS_SRC_NAT_DONE|IPS_DST_NAT_DONE)}, "
+ + "timeout_sec{120}}";
+ assertEquals(expected, event.toString());
+ }
+
+ public static final String CT_V4DELETE_TCP_HEX =
+ // CHECKSTYLE:OFF IndentationCheck
+ // struct nlmsghdr
+ "84000000" + // length = 132
+ "0201" + // type = NFNL_SUBSYS_CTNETLINK (1) << 8 | IPCTNL_MSG_CT_DELETE (2)
+ "0000" + // flags = 0
+ "00000000" + // seqno = 0
+ "00000000" + // pid = 0
+ // struct nfgenmsg
+ "02" + // nfgen_family = AF_INET
+ "00" + // version = NFNETLINK_V0
+ "1234" + // res_id = 0x1234 (big endian)
+ // struct nlattr
+ "3400" + // nla_len = 52
+ "0180" + // nla_type = nested CTA_TUPLE_ORIG
+ // struct nlattr
+ "1400" + // nla_len = 20
+ "0180" + // nla_type = nested CTA_TUPLE_IP
+ "0800 0100 C0A8500C" + // nla_type=CTA_IP_V4_SRC, ip=192.168.80.12
+ "0800 0200 8C700874" + // nla_type=CTA_IP_V4_DST, ip=140.112.8.116
+ // struct nlattr
+ "1C00" + // nla_len = 28
+ "0280" + // nla_type = nested CTA_TUPLE_PROTO
+ "0500 0100 06 000000" + // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+ "0600 0200 F3F1 0000" + // nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)
+ "0600 0300 01BB 0000" + // nla_type=CTA_PROTO_DST_PORT, port=433 (big endian)
+ // struct nlattr
+ "3400" + // nla_len = 52
+ "0280" + // nla_type = nested CTA_TUPLE_REPLY
+ // struct nlattr
+ "1400" + // nla_len = 20
+ "0180" + // nla_type = nested CTA_TUPLE_IP
+ "0800 0100 8C700874" + // nla_type=CTA_IP_V4_SRC, ip=140.112.8.116
+ "0800 0200 6451B301" + // nla_type=CTA_IP_V4_DST, ip=100.81.179.1
+ // struct nlattr
+ "1C00" + // nla_len = 28
+ "0280" + // nla_type = nested CTA_TUPLE_PROTO
+ "0500 0100 06 000000" + // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+ "0600 0200 01BB 0000" + // nla_type=CTA_PROTO_SRC_PORT, port=433 (big endian)
+ "0600 0300 F3F1 0000" + // nla_type=CTA_PROTO_DST_PORT, port=62449 (big endian)
+ // struct nlattr
+ "0800" + // nla_len = 8
+ "0300" + // nla_type = CTA_STATUS
+ "0000039E"; // nla_value = 0b1110011110 (big endian)
+ // IPS_SEEN_REPLY (1 << 1) | IPS_ASSURED (1 << 2) |
+ // IPS_CONFIRMED (1 << 3) | IPS_SRC_NAT (1 << 4) |
+ // IPS_SRC_NAT_DONE (1 << 7) | IPS_DST_NAT_DONE (1 << 8) |
+ // IPS_DYING (1 << 9)
+ // CHECKSTYLE:ON IndentationCheck
+ public static final byte[] CT_V4DELETE_TCP_BYTES =
+ HexEncoding.decode(CT_V4DELETE_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
+
+ @Test
+ public void testConntrackEventDelete() throws Exception {
+ final ConntrackEvent expectedEvent =
+ makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, 0x39e /* status */,
+ 0 /* timeoutSec (absent) */);
+ mConntrackMonitor.sendMessage(CT_V4DELETE_TCP_BYTES);
+ verify(mConsumer, timeout(TIMEOUT_MS)).accept(eq(expectedEvent));
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/ip/InterfaceControllerTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/ip/InterfaceControllerTest.java
new file mode 100644
index 0000000..dea667d
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/ip/InterfaceControllerTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.net.module.util.ip;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.InterfaceConfigurationParcel;
+import android.net.LinkAddress;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.SharedLog;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class InterfaceControllerTest {
+ private static final String TEST_IFACE = "testif";
+ private static final String TEST_IPV4_ADDR = "192.168.123.28";
+ private static final int TEST_PREFIXLENGTH = 31;
+
+ @Mock private INetd mNetd;
+ @Mock private SharedLog mLog;
+ @Captor private ArgumentCaptor<InterfaceConfigurationParcel> mConfigCaptor;
+
+ private InterfaceController mController;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mController = new InterfaceController(TEST_IFACE, mNetd, mLog);
+
+ doNothing().when(mNetd).interfaceSetCfg(mConfigCaptor.capture());
+ }
+
+ @Test
+ public void testSetIPv4Address() throws Exception {
+ mController.setIPv4Address(
+ new LinkAddress(InetAddresses.parseNumericAddress(TEST_IPV4_ADDR),
+ TEST_PREFIXLENGTH));
+ verify(mNetd, times(1)).interfaceSetCfg(any());
+ final InterfaceConfigurationParcel parcel = mConfigCaptor.getValue();
+ assertEquals(TEST_IFACE, parcel.ifName);
+ assertEquals(TEST_IPV4_ADDR, parcel.ipv4Addr);
+ assertEquals(TEST_PREFIXLENGTH, parcel.prefixLength);
+ assertEquals("", parcel.hwAddr);
+ assertArrayEquals(new String[0], parcel.flags);
+ }
+
+ @Test
+ public void testClearIPv4Address() throws Exception {
+ mController.clearIPv4Address();
+ verify(mNetd, times(1)).interfaceSetCfg(any());
+ final InterfaceConfigurationParcel parcel = mConfigCaptor.getValue();
+ assertEquals(TEST_IFACE, parcel.ifName);
+ assertEquals("0.0.0.0", parcel.ipv4Addr);
+ assertEquals(0, parcel.prefixLength);
+ assertEquals("", parcel.hwAddr);
+ assertArrayEquals(new String[0], parcel.flags);
+ }
+}