diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp index a7a027cb38..4fc93ce7a2 100644 --- a/staticlibs/Android.bp +++ b/staticlibs/Android.bp @@ -38,6 +38,7 @@ java_library { "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 @@ filegroup { 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: [ @@ -124,6 +125,36 @@ java_library { ], } +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: [ @@ -205,7 +236,11 @@ filegroup { 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 0000000000..37c6f6dc23 --- /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. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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 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 0000000000..420a54460b --- /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 0000000000..7277fec0b1 --- /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. + * + *

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 0000000000..88f8c9d37d --- /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 0000000000..85898761ab --- /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 0000000000..30762ebb5b --- /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 0000000000..c2416802a7 --- /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 23835fe918..a9f9d70b27 100644 --- a/staticlibs/tests/unit/Android.bp +++ b/staticlibs/tests/unit/Android.bp @@ -13,10 +13,12 @@ android_library { 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 0000000000..446e8819fd --- /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 0000000000..99d8f0739d --- /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 0000000000..dea667d815 --- /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 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); + } +}