| /* |
| * Copyright (C) 2024 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 android.net.apf; |
| |
| import static android.net.apf.ApfConstant.APF_MAX_ETH_TYPE_BLACK_LIST_LEN; |
| import static android.net.apf.ApfConstant.ARP_HEADER_OFFSET; |
| import static android.net.apf.ApfConstant.ARP_IPV4_HEADER; |
| import static android.net.apf.ApfConstant.ARP_OPCODE_OFFSET; |
| import static android.net.apf.ApfConstant.ARP_OPCODE_REPLY; |
| import static android.net.apf.ApfConstant.ARP_OPCODE_REQUEST; |
| import static android.net.apf.ApfConstant.ARP_SOURCE_IP_ADDRESS_OFFSET; |
| import static android.net.apf.ApfConstant.ARP_TARGET_IP_ADDRESS_OFFSET; |
| import static android.net.apf.ApfConstant.DHCP_CLIENT_MAC_OFFSET; |
| import static android.net.apf.ApfConstant.DHCP_CLIENT_PORT; |
| import static android.net.apf.ApfConstant.ECHO_PORT; |
| import static android.net.apf.ApfConstant.ETH_DEST_ADDR_OFFSET; |
| import static android.net.apf.ApfConstant.ETH_ETHERTYPE_OFFSET; |
| import static android.net.apf.ApfConstant.ETH_HEADER_LEN; |
| import static android.net.apf.ApfConstant.ETH_MULTICAST_MDNS_V4_MAC_ADDRESS; |
| import static android.net.apf.ApfConstant.ETH_MULTICAST_MDNS_V6_MAC_ADDRESS; |
| import static android.net.apf.ApfConstant.ETH_TYPE_MAX; |
| import static android.net.apf.ApfConstant.ETH_TYPE_MIN; |
| import static android.net.apf.ApfConstant.FIXED_ARP_REPLY_HEADER; |
| import static android.net.apf.ApfConstant.ICMP6_TYPE_OFFSET; |
| import static android.net.apf.ApfConstant.IPPROTO_HOPOPTS; |
| import static android.net.apf.ApfConstant.IPV4_ANY_HOST_ADDRESS; |
| import static android.net.apf.ApfConstant.IPV4_BROADCAST_ADDRESS; |
| import static android.net.apf.ApfConstant.IPV4_DEST_ADDR_OFFSET; |
| import static android.net.apf.ApfConstant.IPV4_FRAGMENT_MORE_FRAGS_MASK; |
| import static android.net.apf.ApfConstant.IPV4_FRAGMENT_OFFSET_MASK; |
| import static android.net.apf.ApfConstant.IPV4_FRAGMENT_OFFSET_OFFSET; |
| import static android.net.apf.ApfConstant.IPV4_PROTOCOL_OFFSET; |
| import static android.net.apf.ApfConstant.IPV4_TOTAL_LENGTH_OFFSET; |
| import static android.net.apf.ApfConstant.IPV6_ALL_NODES_ADDRESS; |
| import static android.net.apf.ApfConstant.IPV6_DEST_ADDR_OFFSET; |
| import static android.net.apf.ApfConstant.IPV6_FLOW_LABEL_LEN; |
| import static android.net.apf.ApfConstant.IPV6_FLOW_LABEL_OFFSET; |
| import static android.net.apf.ApfConstant.IPV6_HEADER_LEN; |
| import static android.net.apf.ApfConstant.IPV6_NEXT_HEADER_OFFSET; |
| import static android.net.apf.ApfConstant.IPV6_SRC_ADDR_OFFSET; |
| import static android.net.apf.ApfConstant.MDNS_PORT; |
| import static android.net.apf.ApfConstant.MDNS_QDCOUNT_OFFSET; |
| import static android.net.apf.ApfConstant.MDNS_QNAME_OFFSET; |
| import static android.net.apf.ApfConstant.TCP_HEADER_SIZE_OFFSET; |
| import static android.net.apf.ApfConstant.TCP_UDP_DESTINATION_PORT_OFFSET; |
| import static android.net.apf.BaseApfGenerator.MemorySlot; |
| import static android.net.apf.BaseApfGenerator.Register.R0; |
| import static android.net.apf.BaseApfGenerator.Register.R1; |
| import static android.net.util.SocketUtils.makePacketSocketAddress; |
| import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED; |
| import static android.os.PowerManager.ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED; |
| import static android.system.OsConstants.AF_PACKET; |
| import static android.system.OsConstants.ARPHRD_ETHER; |
| import static android.system.OsConstants.ETH_P_ARP; |
| import static android.system.OsConstants.ETH_P_IP; |
| import static android.system.OsConstants.ETH_P_IPV6; |
| import static android.system.OsConstants.IPPROTO_ICMPV6; |
| import static android.system.OsConstants.IPPROTO_TCP; |
| import static android.system.OsConstants.IPPROTO_UDP; |
| import static android.system.OsConstants.SOCK_CLOEXEC; |
| import static android.system.OsConstants.SOCK_RAW; |
| |
| import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN; |
| import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST; |
| import static com.android.net.module.util.NetworkStackConstants.ETHER_SRC_ADDR_OFFSET; |
| import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE; |
| import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT; |
| import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT; |
| import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION; |
| import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_LEN; |
| import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN; |
| |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.net.LinkAddress; |
| import android.net.LinkProperties; |
| import android.net.NattKeepalivePacketDataParcelable; |
| import android.net.TcpKeepalivePacketDataParcelable; |
| import android.net.apf.ApfCounterTracker.Counter; |
| import android.net.apf.BaseApfGenerator.IllegalInstructionException; |
| import android.net.ip.IpClient.IpClientCallbacksWrapper; |
| import android.os.PowerManager; |
| import android.os.SystemClock; |
| import android.stats.connectivity.NetworkQuirkEvent; |
| import android.system.ErrnoException; |
| import android.system.Os; |
| import android.text.format.DateUtils; |
| import android.util.Log; |
| import android.util.SparseArray; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.util.HexDump; |
| import com.android.internal.util.IndentingPrintWriter; |
| import com.android.internal.util.TokenBucket; |
| import com.android.modules.utils.build.SdkLevel; |
| import com.android.net.module.util.CollectionUtils; |
| import com.android.net.module.util.ConnectivityUtils; |
| import com.android.net.module.util.InterfaceParams; |
| import com.android.net.module.util.SocketUtils; |
| import com.android.networkstack.metrics.ApfSessionInfoMetrics; |
| import com.android.networkstack.metrics.IpClientRaInfoMetrics; |
| import com.android.networkstack.metrics.NetworkQuirkMetrics; |
| import com.android.networkstack.util.NetworkStackUtils; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.FileDescriptor; |
| import java.io.IOException; |
| import java.net.Inet4Address; |
| import java.net.InetAddress; |
| import java.net.SocketAddress; |
| import java.net.SocketException; |
| import java.net.UnknownHostException; |
| import java.nio.BufferUnderflowException; |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| import java.nio.charset.StandardCharsets; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * For networks that support packet filtering via APF programs, {@code ApfFilter} |
| * listens for IPv6 ICMPv6 router advertisements (RAs) and generates APF programs to |
| * filter out redundant duplicate ones. |
| * <p> |
| * Threading model: |
| * A collection of RAs we've received is kept in mRas. Generating APF programs uses mRas to |
| * know what RAs to filter for, thus generating APF programs is dependent on mRas. |
| * mRas can be accessed by multiple threads: |
| * - ReceiveThread, which listens for RAs and adds them to mRas, and generates APF programs. |
| * - callers of: |
| * - setMulticastFilter(), which can cause an APF program to be generated. |
| * - dump(), which dumps mRas among other things. |
| * - shutdown(), which clears mRas. |
| * So access to mRas is synchronized. |
| * |
| * @hide |
| */ |
| public class ApfFilter implements AndroidPacketFilter { |
| |
| // Helper class for specifying functional filter parameters. |
| public static class ApfConfiguration { |
| public ApfCapabilities apfCapabilities; |
| public int installableProgramSizeClamp = Integer.MAX_VALUE; |
| public boolean multicastFilter; |
| public boolean ieee802_3Filter; |
| public int[] ethTypeBlackList; |
| public int minRdnssLifetimeSec; |
| public int acceptRaMinLft; |
| public boolean shouldHandleLightDoze; |
| public long minMetricsSessionDurationMs; |
| public boolean hasClatInterface; |
| } |
| |
| /** A wrapper class of {@link SystemClock} to be mocked in unit tests. */ |
| public static class Clock { |
| /** |
| * @see SystemClock#elapsedRealtime |
| */ |
| public long elapsedRealtime() { |
| return SystemClock.elapsedRealtime(); |
| } |
| } |
| |
| // Thread to listen for RAs. |
| @VisibleForTesting |
| public class ReceiveThread extends Thread { |
| private final byte[] mPacket = new byte[1514]; |
| private final FileDescriptor mSocket; |
| |
| private volatile boolean mStopped; |
| |
| public ReceiveThread(FileDescriptor socket) { |
| mSocket = socket; |
| } |
| |
| public void halt() { |
| mStopped = true; |
| // Interrupts the read() call the thread is blocked in. |
| SocketUtils.closeSocketQuietly(mSocket); |
| } |
| |
| @Override |
| public void run() { |
| log("begin monitoring"); |
| while (!mStopped) { |
| try { |
| int length = Os.read(mSocket, mPacket, 0, mPacket.length); |
| processRa(mPacket, length); |
| } catch (IOException|ErrnoException e) { |
| if (!mStopped) { |
| Log.e(TAG, "Read error", e); |
| } |
| } |
| } |
| } |
| } |
| |
| private static final String TAG = "ApfFilter"; |
| private static final boolean DBG = true; |
| private static final boolean VDBG = false; |
| |
| private final ApfCapabilities mApfCapabilities; |
| private final int mInstallableProgramSizeClamp; |
| private final IpClientCallbacksWrapper mIpClientCallback; |
| private final InterfaceParams mInterfaceParams; |
| private final TokenBucket mTokenBucket; |
| |
| @VisibleForTesting |
| public byte[] mHardwareAddress; |
| @VisibleForTesting |
| public ReceiveThread mReceiveThread; |
| @GuardedBy("this") |
| private long mUniqueCounter; |
| @GuardedBy("this") |
| private boolean mMulticastFilter; |
| @GuardedBy("this") |
| private boolean mInDozeMode; |
| private final boolean mDrop802_3Frames; |
| private final int[] mEthTypeBlackList; |
| |
| private final Clock mClock; |
| private final ApfCounterTracker mApfCounterTracker = new ApfCounterTracker(); |
| @GuardedBy("this") |
| private final long mSessionStartMs; |
| @GuardedBy("this") |
| private int mNumParseErrorRas = 0; |
| @GuardedBy("this") |
| private int mNumZeroLifetimeRas = 0; |
| @GuardedBy("this") |
| private int mLowestRouterLifetimeSeconds = Integer.MAX_VALUE; |
| @GuardedBy("this") |
| private long mLowestPioValidLifetimeSeconds = Long.MAX_VALUE; |
| @GuardedBy("this") |
| private long mLowestRioRouteLifetimeSeconds = Long.MAX_VALUE; |
| @GuardedBy("this") |
| private long mLowestRdnssLifetimeSeconds = Long.MAX_VALUE; |
| |
| // Ignore non-zero RDNSS lifetimes below this value. |
| private final int mMinRdnssLifetimeSec; |
| |
| // Minimum session time for metrics, duration less than this time will not be logged. |
| private final long mMinMetricsSessionDurationMs; |
| |
| // Tracks the value of /proc/sys/ipv6/conf/$iface/accept_ra_min_lft which affects router, RIO, |
| // and PIO valid lifetimes. |
| private final int mAcceptRaMinLft; |
| private final boolean mShouldHandleLightDoze; |
| |
| private final NetworkQuirkMetrics mNetworkQuirkMetrics; |
| private final IpClientRaInfoMetrics mIpClientRaInfoMetrics; |
| private final ApfSessionInfoMetrics mApfSessionInfoMetrics; |
| |
| private static boolean isDeviceIdleModeChangedAction(Intent intent) { |
| return ACTION_DEVICE_IDLE_MODE_CHANGED.equals(intent.getAction()); |
| } |
| |
| private boolean isDeviceLightIdleModeChangedAction(Intent intent) { |
| // The ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED only exist since T. For lower platform version, |
| // the check should return false. The explicit SDK check is needed to make linter happy |
| // about accessing ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED in this function. |
| if (!SdkLevel.isAtLeastT()) { |
| return false; |
| } |
| if (!mShouldHandleLightDoze) { |
| return false; |
| } |
| return ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED.equals(intent.getAction()); |
| } |
| |
| private boolean isDeviceLightIdleMode(@NonNull PowerManager powerManager) { |
| // The powerManager.isDeviceLightIdleMode() only exist since T. For lower platform version, |
| // the check should return false. The explicit SDK check is needed to make linter happy |
| // about accessing powerManager.isDeviceLightIdleMode() in this function. |
| if (!SdkLevel.isAtLeastT()) { |
| return false; |
| } |
| if (!mShouldHandleLightDoze) { |
| return false; |
| } |
| |
| return powerManager.isDeviceLightIdleMode(); |
| } |
| |
| // Detects doze mode state transitions. |
| private final BroadcastReceiver mDeviceIdleReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| final PowerManager powerManager = context.getSystemService(PowerManager.class); |
| if (isDeviceIdleModeChangedAction(intent) |
| || isDeviceLightIdleModeChangedAction(intent)) { |
| final boolean deviceIdle = powerManager.isDeviceIdleMode() |
| || isDeviceLightIdleMode(powerManager); |
| setDozeMode(deviceIdle); |
| } |
| } |
| }; |
| |
| // Our IPv4 address, if we have just one, otherwise null. |
| @GuardedBy("this") |
| private byte[] mIPv4Address; |
| // The subnet prefix length of our IPv4 network. Only valid if mIPv4Address is not null. |
| @GuardedBy("this") |
| private int mIPv4PrefixLength; |
| |
| // Whether CLAT is enabled. |
| @GuardedBy("this") |
| private boolean mHasClat; |
| |
| // mIsRunning is reflects the state of the ApfFilter during integration tests. ApfFilter can be |
| // paused using "adb shell cmd apf <iface> <cmd>" commands. A paused ApfFilter will not install |
| // any new programs, but otherwise operates normally. |
| private volatile boolean mIsRunning = true; |
| |
| private final Dependencies mDependencies; |
| |
| public ApfFilter(Context context, ApfConfiguration config, InterfaceParams ifParams, |
| IpClientCallbacksWrapper ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics) { |
| this(context, config, ifParams, ipClientCallback, networkQuirkMetrics, |
| new Dependencies(context), new Clock()); |
| } |
| |
| @VisibleForTesting |
| public ApfFilter(Context context, ApfConfiguration config, InterfaceParams ifParams, |
| IpClientCallbacksWrapper ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics, |
| Dependencies dependencies) { |
| this(context, config, ifParams, ipClientCallback, networkQuirkMetrics, dependencies, |
| new Clock()); |
| } |
| |
| @VisibleForTesting |
| public ApfFilter(Context context, ApfConfiguration config, InterfaceParams ifParams, |
| IpClientCallbacksWrapper ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics, |
| Dependencies dependencies, Clock clock) { |
| mApfCapabilities = config.apfCapabilities; |
| mInstallableProgramSizeClamp = config.installableProgramSizeClamp; |
| mIpClientCallback = ipClientCallback; |
| mInterfaceParams = ifParams; |
| mMulticastFilter = config.multicastFilter; |
| mDrop802_3Frames = config.ieee802_3Filter; |
| mMinRdnssLifetimeSec = config.minRdnssLifetimeSec; |
| mAcceptRaMinLft = config.acceptRaMinLft; |
| mShouldHandleLightDoze = config.shouldHandleLightDoze; |
| mDependencies = dependencies; |
| mNetworkQuirkMetrics = networkQuirkMetrics; |
| mIpClientRaInfoMetrics = dependencies.getIpClientRaInfoMetrics(); |
| mApfSessionInfoMetrics = dependencies.getApfSessionInfoMetrics(); |
| mClock = clock; |
| mSessionStartMs = mClock.elapsedRealtime(); |
| mMinMetricsSessionDurationMs = config.minMetricsSessionDurationMs; |
| mHasClat = config.hasClatInterface; |
| |
| // Now fill the black list from the passed array |
| mEthTypeBlackList = filterEthTypeBlackList(config.ethTypeBlackList); |
| |
| // TokenBucket for rate limiting filter installation. APF filtering relies on the filter |
| // always being up-to-date and APF bytecode being in sync with userspace. The TokenBucket |
| // merely prevents illconfigured / abusive networks from impacting the system, so it does |
| // not need to be very restrictive. |
| // The TokenBucket starts with its full capacity of 20 tokens (= 20 filter updates). A new |
| // token is generated every 3 seconds limiting the filter update rate to at most once every |
| // 3 seconds. |
| mTokenBucket = new TokenBucket(3_000 /* deltaMs */, 20 /* capacity */, 20 /* tokens */); |
| |
| // TODO: ApfFilter should not generate programs until IpClient sends provisioning success. |
| maybeStartFilter(); |
| |
| // Listen for doze-mode transition changes to enable/disable the IPv6 multicast filter. |
| mDependencies.addDeviceIdleReceiver(mDeviceIdleReceiver, mShouldHandleLightDoze); |
| |
| mDependencies.onApfFilterCreated(this); |
| // mReceiveThread is created in maybeStartFilter() and halted in shutdown(). |
| mDependencies.onThreadCreated(mReceiveThread); |
| } |
| |
| /** |
| * Dependencies class for testing. |
| */ |
| @VisibleForTesting |
| public static class Dependencies { |
| private final Context mContext; |
| public Dependencies(final Context context) { |
| mContext = context; |
| } |
| |
| /** Add receiver for detecting doze mode change */ |
| public void addDeviceIdleReceiver(@NonNull final BroadcastReceiver receiver, |
| boolean shouldHandleLightDoze) { |
| final IntentFilter intentFilter = new IntentFilter(ACTION_DEVICE_IDLE_MODE_CHANGED); |
| if (SdkLevel.isAtLeastT() && shouldHandleLightDoze) { |
| intentFilter.addAction(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED); |
| } |
| mContext.registerReceiver(receiver, intentFilter); |
| } |
| |
| /** Remove broadcast receiver. */ |
| public void removeBroadcastReceiver(@NonNull final BroadcastReceiver receiver) { |
| mContext.unregisterReceiver(receiver); |
| } |
| |
| /** |
| * Get a ApfSessionInfoMetrics instance. |
| */ |
| public ApfSessionInfoMetrics getApfSessionInfoMetrics() { |
| return new ApfSessionInfoMetrics(); |
| } |
| |
| /** |
| * Get a IpClientRaInfoMetrics instance. |
| */ |
| public IpClientRaInfoMetrics getIpClientRaInfoMetrics() { |
| return new IpClientRaInfoMetrics(); |
| } |
| |
| /** |
| * Callback to be called when an ApfFilter instance is created. |
| * |
| * This method is designed to be overridden in test classes to collect created ApfFilter |
| * instances. |
| */ |
| public void onApfFilterCreated(@NonNull AndroidPacketFilter apfFilter) { |
| } |
| |
| /** |
| * Callback to be called when a ReceiveThread instance is created. |
| * |
| * This method is designed for overriding in test classes to collect created threads and |
| * waits for the termination. |
| */ |
| public void onThreadCreated(@NonNull Thread thread) { |
| } |
| } |
| |
| public synchronized void setDataSnapshot(byte[] data) { |
| mDataSnapshot = data; |
| if (mIsRunning) { |
| mApfCounterTracker.updateCountersFromData(data); |
| } |
| } |
| |
| private void log(String s) { |
| Log.d(TAG, "(" + mInterfaceParams.name + "): " + s); |
| } |
| |
| @GuardedBy("this") |
| private long getUniqueNumberLocked() { |
| return mUniqueCounter++; |
| } |
| |
| private static int[] filterEthTypeBlackList(int[] ethTypeBlackList) { |
| ArrayList<Integer> bl = new ArrayList<>(); |
| |
| for (int p : ethTypeBlackList) { |
| // Check if the protocol is a valid ether type |
| if ((p < ETH_TYPE_MIN) || (p > ETH_TYPE_MAX)) { |
| continue; |
| } |
| |
| // Check if the protocol is not repeated in the passed array |
| if (bl.contains(p)) { |
| continue; |
| } |
| |
| // Check if list reach its max size |
| if (bl.size() == APF_MAX_ETH_TYPE_BLACK_LIST_LEN) { |
| Log.w(TAG, "Passed EthType Black List size too large (" + bl.size() + |
| ") using top " + APF_MAX_ETH_TYPE_BLACK_LIST_LEN + " protocols"); |
| break; |
| } |
| |
| // Now add the protocol to the list |
| bl.add(p); |
| } |
| |
| return bl.stream().mapToInt(Integer::intValue).toArray(); |
| } |
| |
| /** |
| * Attempt to start listening for RAs and, if RAs are received, generating and installing |
| * filters to ignore useless RAs. |
| */ |
| @VisibleForTesting |
| public void maybeStartFilter() { |
| FileDescriptor socket; |
| try { |
| mHardwareAddress = mInterfaceParams.macAddr.toByteArray(); |
| synchronized(this) { |
| // Clear the APF memory to reset all counters upon connecting to the first AP |
| // in an SSID. This is limited to APFv4 devices because this large write triggers |
| // a crash on some older devices (b/78905546). |
| if (mIsRunning && mApfCapabilities.hasDataAccess()) { |
| byte[] zeroes = new byte[mApfCapabilities.maximumApfProgramSize]; |
| if (!mIpClientCallback.installPacketFilter(zeroes)) { |
| sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE); |
| } |
| } |
| |
| // Install basic filters |
| installNewProgramLocked(); |
| } |
| socket = Os.socket(AF_PACKET, SOCK_RAW | SOCK_CLOEXEC, 0); |
| NetworkStackUtils.attachRaFilter(socket); |
| SocketAddress addr = makePacketSocketAddress(ETH_P_IPV6, mInterfaceParams.index); |
| Os.bind(socket, addr); |
| } catch(SocketException|ErrnoException e) { |
| Log.e(TAG, "Error starting filter", e); |
| return; |
| } |
| mReceiveThread = new ReceiveThread(socket); |
| mReceiveThread.start(); |
| } |
| |
| // Returns seconds since device boot. |
| @VisibleForTesting |
| protected int secondsSinceBoot() { |
| return (int) (mClock.elapsedRealtime() / DateUtils.SECOND_IN_MILLIS); |
| } |
| |
| public static class InvalidRaException extends Exception { |
| public InvalidRaException(String m) { |
| super(m); |
| } |
| } |
| |
| /** |
| * Class to keep track of a section in a packet. |
| */ |
| private static class PacketSection { |
| public enum Type { |
| MATCH, // A field that should be matched (e.g., the router IP address). |
| LIFETIME, // A lifetime. Not matched, and counts toward minimum RA lifetime if >= min. |
| } |
| |
| /** The type of section. */ |
| public final Type type; |
| /** Offset into the packet at which this section begins. */ |
| public final int start; |
| /** Length of this section in bytes. */ |
| public final int length; |
| /** If this is a lifetime, the lifetime value. */ |
| public final long lifetime; |
| /** If this is a lifetime, the value below which the lifetime is ignored */ |
| public final int min; |
| |
| PacketSection(int start, int length, Type type, long lifetime, int min) { |
| this.start = start; |
| |
| if (type == Type.LIFETIME && length != 2 && length != 4) { |
| throw new IllegalArgumentException("LIFETIME section length must be 2 or 4 bytes"); |
| } |
| this.length = length; |
| this.type = type; |
| |
| if (type == Type.MATCH && (lifetime != 0 || min != 0)) { |
| throw new IllegalArgumentException("lifetime, min must be 0 for MATCH sections"); |
| } |
| this.lifetime = lifetime; |
| |
| // It has already been asserted that min is 0 for MATCH sections. |
| if (min < 0) { |
| throw new IllegalArgumentException("min must be >= 0 for LIFETIME sections"); |
| } |
| this.min = min; |
| } |
| |
| public String toString() { |
| if (type == Type.LIFETIME) { |
| return String.format("%s: (%d, %d) %d %d", type, start, length, lifetime, min); |
| } else { |
| return String.format("%s: (%d, %d)", type, start, length); |
| } |
| } |
| } |
| |
| // A class to hold information about an RA. |
| @VisibleForTesting |
| public class Ra { |
| // From RFC4861: |
| private static final int ICMP6_RA_HEADER_LEN = 16; |
| private static final int ICMP6_RA_CHECKSUM_OFFSET = |
| ETH_HEADER_LEN + IPV6_HEADER_LEN + 2; |
| private static final int ICMP6_RA_CHECKSUM_LEN = 2; |
| private static final int ICMP6_RA_OPTION_OFFSET = |
| ETH_HEADER_LEN + IPV6_HEADER_LEN + ICMP6_RA_HEADER_LEN; |
| private static final int ICMP6_RA_ROUTER_LIFETIME_OFFSET = |
| ETH_HEADER_LEN + IPV6_HEADER_LEN + 6; |
| private static final int ICMP6_RA_ROUTER_LIFETIME_LEN = 2; |
| // Prefix information option. |
| private static final int ICMP6_PREFIX_OPTION_TYPE = 3; |
| private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET = 4; |
| private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN = 4; |
| private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN = 4; |
| |
| // From RFC4861: source link-layer address |
| private static final int ICMP6_SOURCE_LL_ADDRESS_OPTION_TYPE = 1; |
| // From RFC4861: mtu size option |
| private static final int ICMP6_MTU_OPTION_TYPE = 5; |
| // From RFC6106: Recursive DNS Server option |
| private static final int ICMP6_RDNSS_OPTION_TYPE = 25; |
| // From RFC6106: DNS Search List option |
| private static final int ICMP6_DNSSL_OPTION_TYPE = 31; |
| // From RFC8910: Captive-Portal option |
| private static final int ICMP6_CAPTIVE_PORTAL_OPTION_TYPE = 37; |
| // From RFC8781: PREF64 option |
| private static final int ICMP6_PREF64_OPTION_TYPE = 38; |
| |
| // From RFC4191: Route Information option |
| private static final int ICMP6_ROUTE_INFO_OPTION_TYPE = 24; |
| // Above three options all have the same format: |
| private static final int ICMP6_4_BYTE_LIFETIME_OFFSET = 4; |
| private static final int ICMP6_4_BYTE_LIFETIME_LEN = 4; |
| |
| // Note: mPacket's position() cannot be assumed to be reset. |
| private final ByteBuffer mPacket; |
| |
| // List of sections in the packet. |
| private final ArrayList<PacketSection> mPacketSections = new ArrayList<>(); |
| |
| // Router lifetime in packet |
| private final int mRouterLifetime; |
| // Minimum valid lifetime of PIOs in packet, Long.MAX_VALUE means not seen. |
| private long mMinPioValidLifetime = Long.MAX_VALUE; |
| // Minimum route lifetime of RIOs in packet, Long.MAX_VALUE means not seen. |
| private long mMinRioRouteLifetime = Long.MAX_VALUE; |
| // Minimum lifetime of RDNSSs in packet, Long.MAX_VALUE means not seen. |
| private long mMinRdnssLifetime = Long.MAX_VALUE; |
| // The time in seconds in which some of the information contained in this RA expires. |
| private final int mExpirationTime; |
| // When the packet was last captured, in seconds since Unix Epoch |
| private final int mLastSeen; |
| |
| // For debugging only. Offsets into the packet where PIOs are. |
| private final ArrayList<Integer> mPrefixOptionOffsets = new ArrayList<>(); |
| |
| // For debugging only. Offsets into the packet where RDNSS options are. |
| private final ArrayList<Integer> mRdnssOptionOffsets = new ArrayList<>(); |
| |
| // For debugging only. Offsets into the packet where RIO options are. |
| private final ArrayList<Integer> mRioOptionOffsets = new ArrayList<>(); |
| |
| // For debugging only. Returns the hex representation of the last matching packet. |
| String getLastMatchingPacket() { |
| return HexDump.toHexString(mPacket.array(), 0, mPacket.capacity(), |
| false /* lowercase */); |
| } |
| |
| // For debugging only. Returns the string representation of the IPv6 address starting at |
| // position pos in the packet. |
| private String IPv6AddresstoString(int pos) { |
| try { |
| byte[] array = mPacket.array(); |
| // Can't just call copyOfRange() and see if it throws, because if it reads past the |
| // end it pads with zeros instead of throwing. |
| if (pos < 0 || pos + 16 > array.length || pos + 16 < pos) { |
| return "???"; |
| } |
| byte[] addressBytes = Arrays.copyOfRange(array, pos, pos + 16); |
| InetAddress address = InetAddress.getByAddress(addressBytes); |
| return address.getHostAddress(); |
| } catch (UnsupportedOperationException e) { |
| // array() failed. Cannot happen, mPacket is array-backed and read-write. |
| return "???"; |
| } catch (ClassCastException|UnknownHostException e) { |
| // Cannot happen. |
| return "???"; |
| } |
| } |
| |
| // Can't be static because it's in a non-static inner class. |
| // TODO: Make this static once RA is its own class. |
| private void prefixOptionToString(StringBuffer sb, int offset) { |
| String prefix = IPv6AddresstoString(offset + 16); |
| int length = getUint8(mPacket, offset + 2); |
| long valid = getUint32(mPacket, offset + 4); |
| long preferred = getUint32(mPacket, offset + 8); |
| sb.append(String.format("%s/%d %ds/%ds ", prefix, length, valid, preferred)); |
| } |
| |
| private void rdnssOptionToString(StringBuffer sb, int offset) { |
| int optLen = getUint8(mPacket, offset + 1) * 8; |
| if (optLen < 24) return; // Malformed or empty. |
| long lifetime = getUint32(mPacket, offset + 4); |
| int numServers = (optLen - 8) / 16; |
| sb.append("DNS ").append(lifetime).append("s"); |
| for (int server = 0; server < numServers; server++) { |
| sb.append(" ").append(IPv6AddresstoString(offset + 8 + 16 * server)); |
| } |
| sb.append(" "); |
| } |
| |
| private void rioOptionToString(StringBuffer sb, int offset) { |
| int optLen = getUint8(mPacket, offset + 1) * 8; |
| if (optLen < 8 || optLen > 24) return; // Malformed or empty. |
| int prefixLen = getUint8(mPacket, offset + 2); |
| long lifetime = getUint32(mPacket, offset + 4); |
| |
| // This read is variable length because the prefix can be 0, 8 or 16 bytes long. |
| // We can't use any of the ByteBuffer#get methods here because they all start reading |
| // from the buffer's current position. |
| byte[] prefix = new byte[IPV6_ADDR_LEN]; |
| System.arraycopy(mPacket.array(), offset + 8, prefix, 0, optLen - 8); |
| sb.append("RIO ").append(lifetime).append("s "); |
| try { |
| InetAddress address = InetAddress.getByAddress(prefix); |
| sb.append(address.getHostAddress()); |
| } catch (UnknownHostException impossible) { |
| sb.append("???"); |
| } |
| sb.append("/").append(prefixLen).append(" "); |
| } |
| |
| public String toString() { |
| try { |
| StringBuffer sb = new StringBuffer(); |
| sb.append(String.format("RA %s -> %s %ds ", |
| IPv6AddresstoString(IPV6_SRC_ADDR_OFFSET), |
| IPv6AddresstoString(IPV6_DEST_ADDR_OFFSET), |
| getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET))); |
| for (int i: mPrefixOptionOffsets) { |
| prefixOptionToString(sb, i); |
| } |
| for (int i: mRdnssOptionOffsets) { |
| rdnssOptionToString(sb, i); |
| } |
| for (int i: mRioOptionOffsets) { |
| rioOptionToString(sb, i); |
| } |
| return sb.toString(); |
| } catch (BufferUnderflowException|IndexOutOfBoundsException e) { |
| return "<Malformed RA>"; |
| } |
| } |
| |
| /** |
| * Add a packet section that should be matched, starting from the current position. |
| * @param length the length of the section |
| */ |
| private void addMatchSection(int length) { |
| // Don't generate JNEBS instruction for 0 bytes as they will fail the |
| // ASSERT_FORWARD_IN_PROGRAM(pc + cmp_imm - 1) check (where cmp_imm is |
| // the number of bytes to compare) and immediately pass the packet. |
| // The code does not attempt to generate such matches, but add a safety |
| // check to prevent doing so in the presence of bugs or malformed or |
| // truncated packets. |
| if (length == 0) return; |
| |
| // we need to add a MATCH section 'from, length, MATCH, 0, 0' |
| int from = mPacket.position(); |
| |
| // if possible try to increase the length of the previous match section |
| int lastIdx = mPacketSections.size() - 1; |
| if (lastIdx >= 0) { // there had to be a previous section |
| PacketSection prev = mPacketSections.get(lastIdx); |
| if (prev.type == PacketSection.Type.MATCH) { // of type match |
| if (prev.start + prev.length == from) { // ending where we start |
| from -= prev.length; |
| length += prev.length; |
| mPacketSections.remove(lastIdx); |
| } |
| } |
| } |
| |
| mPacketSections.add(new PacketSection(from, length, PacketSection.Type.MATCH, 0, 0)); |
| mPacket.position(from + length); |
| } |
| |
| /** |
| * Add a packet section that should be matched, starting from the current position. |
| * @param end the offset in the packet before which the section ends |
| */ |
| private void addMatchUntil(int end) { |
| addMatchSection(end - mPacket.position()); |
| } |
| |
| /** |
| * Add a packet section that should be ignored, starting from the current position. |
| * @param length the length of the section in bytes |
| */ |
| private void addIgnoreSection(int length) { |
| mPacket.position(mPacket.position() + length); |
| } |
| |
| /** |
| * Add a packet section that represents a lifetime, starting from the current position. |
| * @param length the length of the section in bytes |
| * @param lifetime the lifetime |
| * @param min the minimum acceptable lifetime |
| */ |
| private void addLifetimeSection(int length, long lifetime, int min) { |
| mPacketSections.add( |
| new PacketSection(mPacket.position(), length, PacketSection.Type.LIFETIME, |
| lifetime, min)); |
| mPacket.position(mPacket.position() + length); |
| } |
| |
| /** |
| * Adds packet sections for an RA option with a 4-byte lifetime 4 bytes into the option |
| * @param optionLength the length of the option in bytes |
| * @param min the minimum acceptable lifetime |
| */ |
| private long add4ByteLifetimeOption(int optionLength, int min) { |
| addMatchSection(ICMP6_4_BYTE_LIFETIME_OFFSET); |
| final long lifetime = getUint32(mPacket, mPacket.position()); |
| addLifetimeSection(ICMP6_4_BYTE_LIFETIME_LEN, lifetime, min); |
| addMatchSection(optionLength - ICMP6_4_BYTE_LIFETIME_OFFSET |
| - ICMP6_4_BYTE_LIFETIME_LEN); |
| return lifetime; |
| } |
| |
| /** |
| * Return the router lifetime of the RA |
| */ |
| public int routerLifetime() { |
| return mRouterLifetime; |
| } |
| |
| /** |
| * Return the minimum valid lifetime in PIOs |
| */ |
| public long minPioValidLifetime() { |
| return mMinPioValidLifetime; |
| } |
| |
| /** |
| * Return the minimum route lifetime in RIOs |
| */ |
| public long minRioRouteLifetime() { |
| return mMinRioRouteLifetime; |
| } |
| |
| /** |
| * Return the minimum lifetime in RDNSSs |
| */ |
| public long minRdnssLifetime() { |
| return mMinRdnssLifetime; |
| } |
| |
| // Note that this parses RA and may throw InvalidRaException (from |
| // Buffer.position(int) or due to an invalid-length option) or IndexOutOfBoundsException |
| // (from ByteBuffer.get(int) ) if parsing encounters something non-compliant with |
| // specifications. |
| @VisibleForTesting |
| public Ra(byte[] packet, int length) throws InvalidRaException { |
| if (length < ICMP6_RA_OPTION_OFFSET) { |
| throw new InvalidRaException("Not an ICMP6 router advertisement: too short"); |
| } |
| |
| mPacket = ByteBuffer.wrap(Arrays.copyOf(packet, length)); |
| mLastSeen = secondsSinceBoot(); |
| |
| // Check packet in case a packet arrives before we attach RA filter |
| // to our packet socket. b/29586253 |
| if (getUint16(mPacket, ETH_ETHERTYPE_OFFSET) != ETH_P_IPV6 || |
| getUint8(mPacket, IPV6_NEXT_HEADER_OFFSET) != IPPROTO_ICMPV6 || |
| getUint8(mPacket, ICMP6_TYPE_OFFSET) != ICMPV6_ROUTER_ADVERTISEMENT) { |
| throw new InvalidRaException("Not an ICMP6 router advertisement"); |
| } |
| |
| // Ignore the flow label and low 4 bits of traffic class. |
| addMatchUntil(IPV6_FLOW_LABEL_OFFSET); |
| addIgnoreSection(IPV6_FLOW_LABEL_LEN); |
| |
| // Ignore IPv6 destination address. |
| addMatchUntil(IPV6_DEST_ADDR_OFFSET); |
| addIgnoreSection(IPV6_ADDR_LEN); |
| |
| // Ignore checksum. |
| addMatchUntil(ICMP6_RA_CHECKSUM_OFFSET); |
| addIgnoreSection(ICMP6_RA_CHECKSUM_LEN); |
| |
| // Parse router lifetime |
| addMatchUntil(ICMP6_RA_ROUTER_LIFETIME_OFFSET); |
| mRouterLifetime = getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET); |
| addLifetimeSection(ICMP6_RA_ROUTER_LIFETIME_LEN, mRouterLifetime, mAcceptRaMinLft); |
| if (mRouterLifetime == 0) mNumZeroLifetimeRas++; |
| |
| // Add remaining fields (reachable time and retransmission timer) to match section. |
| addMatchUntil(ICMP6_RA_OPTION_OFFSET); |
| |
| while (mPacket.hasRemaining()) { |
| final int position = mPacket.position(); |
| final int optionType = getUint8(mPacket, position); |
| final int optionLength = getUint8(mPacket, position + 1) * 8; |
| if (optionLength <= 0) { |
| throw new InvalidRaException(String.format( |
| "Invalid option length opt=%d len=%d", optionType, optionLength)); |
| } |
| |
| long lifetime; |
| switch (optionType) { |
| case ICMP6_PREFIX_OPTION_TYPE: |
| mPrefixOptionOffsets.add(position); |
| |
| // Parse valid lifetime |
| addMatchSection(ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET); |
| lifetime = getUint32(mPacket, mPacket.position()); |
| addLifetimeSection(ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN, |
| lifetime, mAcceptRaMinLft); |
| mMinPioValidLifetime = getMinForPositiveValue( |
| mMinPioValidLifetime, lifetime); |
| if (lifetime == 0) mNumZeroLifetimeRas++; |
| |
| // Parse preferred lifetime |
| lifetime = getUint32(mPacket, mPacket.position()); |
| // The PIO preferred lifetime is not affected by accept_ra_min_lft and |
| // therefore does not have a minimum. |
| addLifetimeSection(ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN, |
| lifetime, 0 /* min lifetime */); |
| |
| addMatchSection(4); // Reserved bytes |
| addMatchSection(IPV6_ADDR_LEN); // The prefix itself |
| break; |
| // These three options have the same lifetime offset and size, and |
| // are processed with the same specialized add4ByteLifetimeOption: |
| case ICMP6_RDNSS_OPTION_TYPE: |
| mRdnssOptionOffsets.add(position); |
| lifetime = add4ByteLifetimeOption(optionLength, mMinRdnssLifetimeSec); |
| mMinRdnssLifetime = getMinForPositiveValue(mMinRdnssLifetime, lifetime); |
| if (lifetime == 0) mNumZeroLifetimeRas++; |
| break; |
| case ICMP6_ROUTE_INFO_OPTION_TYPE: |
| mRioOptionOffsets.add(position); |
| lifetime = add4ByteLifetimeOption(optionLength, mAcceptRaMinLft); |
| mMinRioRouteLifetime = getMinForPositiveValue( |
| mMinRioRouteLifetime, lifetime); |
| if (lifetime == 0) mNumZeroLifetimeRas++; |
| break; |
| case ICMP6_SOURCE_LL_ADDRESS_OPTION_TYPE: |
| case ICMP6_MTU_OPTION_TYPE: |
| case ICMP6_PREF64_OPTION_TYPE: |
| addMatchSection(optionLength); |
| break; |
| case ICMP6_CAPTIVE_PORTAL_OPTION_TYPE: // unlikely to ever change. |
| case ICMP6_DNSSL_OPTION_TYPE: // currently unsupported in userspace. |
| default: |
| // RFC4861 section 4.2 dictates we ignore unknown options for forwards |
| // compatibility. |
| // However, make sure the option's type and length match. |
| addMatchSection(2); // option type & length |
| // optionLength is guaranteed to be >= 8. |
| addIgnoreSection(optionLength - 2); |
| break; |
| } |
| } |
| mExpirationTime = getExpirationTime(); |
| } |
| |
| public enum MatchType { |
| NO_MATCH, // the RAs do not match |
| MATCH_PASS, // the RAS match, and the APF program would pass. |
| MATCH_DROP, // the RAs match, but the APF program would drop. |
| } |
| |
| // Considering only the MATCH sections, does {@code packet} match this RA? |
| MatchType matches(Ra newRa) { |
| // Does their size match? |
| if (newRa.mPacket.capacity() != mPacket.capacity()) return MatchType.NO_MATCH; |
| |
| // If the filter has expired, it cannot match the new RA. |
| if (getRemainingFilterLft(secondsSinceBoot()) <= 0) return MatchType.NO_MATCH; |
| |
| // Check if all MATCH sections are byte-identical. |
| final byte[] newPacket = newRa.mPacket.array(); |
| final byte[] oldPacket = mPacket.array(); |
| for (PacketSection section : mPacketSections) { |
| if (section.type != PacketSection.Type.MATCH) continue; |
| for (int i = section.start; i < (section.start + section.length); i++) { |
| if (newPacket[i] != oldPacket[i]) return MatchType.NO_MATCH; |
| } |
| } |
| |
| // Apply APF lifetime matching to LIFETIME sections and decide whether a packet should |
| // be processed (MATCH_PASS) or ignored (MATCH_DROP). This logic is needed to |
| // consistently process / ignore packets no matter the current state of the APF program. |
| // Note that userspace has no control (or knowledge) over when the APF program is |
| // running. |
| for (PacketSection section : mPacketSections) { |
| if (section.type != PacketSection.Type.LIFETIME) continue; |
| |
| // the lifetime of the new RA. |
| long lft = 0; |
| switch (section.length) { |
| // section.length is guaranteed to be 2 or 4. |
| case 2: lft = getUint16(newRa.mPacket, section.start); break; |
| case 4: lft = getUint32(newRa.mPacket, section.start); break; |
| } |
| |
| // WARNING: keep this in sync with Ra#generateFilterLocked()! |
| if (section.lifetime == 0) { |
| // Case 1) old lft == 0 |
| if (section.min > 0) { |
| // a) in the presence of a min value. |
| // if lft >= min -> PASS |
| // gen.addJumpIfR0GreaterThan(section.min - 1, nextFilterLabel); |
| if (lft >= section.min) return MatchType.MATCH_PASS; |
| } else { |
| // b) if min is 0 / there is no min value. |
| // if lft > 0 -> PASS |
| // gen.addJumpIfR0GreaterThan(0, nextFilterLabel); |
| if (lft > 0) return MatchType.MATCH_PASS; |
| } |
| } else if (section.min == 0) { |
| // Case 2b) section is not affected by any minimum. |
| // |
| // if lft < (oldLft + 2) // 3 -> PASS |
| // if lft > oldLft -> PASS |
| // gen.addJumpIfR0LessThan(((section.lifetime + 2) / 3), |
| // nextFilterLabel); |
| if (lft < (section.lifetime + 2) / 3) return MatchType.MATCH_PASS; |
| // gen.addJumpIfR0GreaterThan(section.lifetime, nextFilterLabel); |
| if (lft > section.lifetime) return MatchType.MATCH_PASS; |
| } else if (section.lifetime < section.min) { |
| // Case 2a) 0 < old lft < min |
| // |
| // if lft == 0 -> PASS |
| // if lft >= min -> PASS |
| // gen.addJumpIfR0Equals(0, nextFilterLabel); |
| if (lft == 0) return MatchType.MATCH_PASS; |
| // gen.addJumpIfR0GreaterThan(section.min - 1, nextFilterLabel); |
| if (lft >= section.min) return MatchType.MATCH_PASS; |
| } else if (section.lifetime <= 3 * (long) section.min) { |
| // Case 3a) min <= old lft <= 3 * min |
| // Note that: |
| // "(old lft + 2) / 3 <= min" is equivalent to "old lft <= 3 * min" |
| // |
| // Essentially, in this range there is no "renumbering support", as the |
| // renumbering constant of 1/3 * old lft is smaller than the minimum |
| // lifetime accepted by the kernel / userspace. |
| // |
| // if lft == 0 -> PASS |
| // if lft > oldLft -> PASS |
| // gen.addJumpIfR0Equals(0, nextFilterLabel); |
| if (lft == 0) return MatchType.MATCH_PASS; |
| // gen.addJumpIfR0GreaterThan(section.lifetime, nextFilterLabel); |
| if (lft > section.lifetime) return MatchType.MATCH_PASS; |
| } else { |
| // Case 4a) otherwise |
| // |
| // if lft == 0 -> PASS |
| // if lft < min -> CONTINUE |
| // if lft < (oldLft + 2) // 3 -> PASS |
| // if lft > oldLft -> PASS |
| // gen.addJumpIfR0Equals(0, nextFilterLabel); |
| if (lft == 0) return MatchType.MATCH_PASS; |
| // gen.addJumpIfR0LessThan(section.min, continueLabel); |
| if (lft < section.min) continue; |
| // gen.addJumpIfR0LessThan(((section.lifetime + 2) / 3), |
| // nextFilterLabel); |
| if (lft < (section.lifetime + 2) / 3) return MatchType.MATCH_PASS; |
| // gen.addJumpIfR0GreaterThan(section.lifetime, nextFilterLabel); |
| if (lft > section.lifetime) return MatchType.MATCH_PASS; |
| } |
| } |
| |
| return MatchType.MATCH_DROP; |
| } |
| |
| // Get the number of seconds in which some of the information contained in this RA expires. |
| private int getExpirationTime() { |
| // While technically most lifetimes in the RA are u32s, as far as the RA filter is |
| // concerned, INT_MAX is still a *much* longer lifetime than any filter would ever |
| // reasonably be active for. |
| // Clamp expirationTime at INT_MAX. |
| int expirationTime = Integer.MAX_VALUE; |
| for (PacketSection section : mPacketSections) { |
| if (section.type != PacketSection.Type.LIFETIME) { |
| continue; |
| } |
| // Ignore lifetimes below section.min and always ignore 0 lifetimes. |
| if (section.lifetime < Math.max(section.min, 1)) { |
| continue; |
| } |
| |
| expirationTime = (int) Math.min(expirationTime, section.lifetime); |
| } |
| return expirationTime; |
| } |
| |
| // Filter for a fraction of the expiration time and adjust for the age of the RA. |
| int getRemainingFilterLft(int currentTimeSeconds) { |
| int filterLifetime = ((mExpirationTime / FRACTION_OF_LIFETIME_TO_FILTER) |
| - (currentTimeSeconds - mLastSeen)); |
| filterLifetime = Math.max(0, filterLifetime); |
| // Clamp filterLifetime to <= 65535, so it fits in 2 bytes. |
| return Math.min(65535, filterLifetime); |
| } |
| |
| // Append a filter for this RA to {@code gen}. Jump to DROP_LABEL if it should be dropped. |
| // Jump to the next filter if packet doesn't match this RA. |
| @GuardedBy("ApfFilter.this") |
| void generateFilterLocked(ApfV4GeneratorBase<?> gen, int timeSeconds) |
| throws IllegalInstructionException { |
| String nextFilterLabel = "Ra" + getUniqueNumberLocked(); |
| // Skip if packet is not the right size |
| gen.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE); |
| gen.addJumpIfR0NotEquals(mPacket.capacity(), nextFilterLabel); |
| // Skip filter if expired |
| gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_SECONDS); |
| gen.addJumpIfR0GreaterThan(getRemainingFilterLft(timeSeconds), nextFilterLabel); |
| for (PacketSection section : mPacketSections) { |
| // Generate code to match the packet bytes. |
| if (section.type == PacketSection.Type.MATCH) { |
| gen.addLoadImmediate(R0, section.start); |
| gen.addJumpIfBytesAtR0NotEqual( |
| Arrays.copyOfRange(mPacket.array(), section.start, |
| section.start + section.length), |
| nextFilterLabel); |
| } else { |
| switch (section.length) { |
| // length asserted to be either 2 or 4 on PacketSection construction |
| case 2: gen.addLoad16(R0, section.start); break; |
| case 4: gen.addLoad32(R0, section.start); break; |
| } |
| |
| // WARNING: keep this in sync with matches()! |
| // For more information on lifetime comparisons in the APF bytecode, see |
| // go/apf-ra-filter. |
| if (section.lifetime == 0) { |
| // Case 1) old lft == 0 |
| if (section.min > 0) { |
| // a) in the presence of a min value. |
| // if lft >= min -> PASS |
| gen.addJumpIfR0GreaterThan(section.min - 1, nextFilterLabel); |
| } else { |
| // b) if min is 0 / there is no min value. |
| // if lft > 0 -> PASS |
| gen.addJumpIfR0GreaterThan(0, nextFilterLabel); |
| } |
| } else if (section.min == 0) { |
| // Case 2b) section is not affected by any minimum. |
| // |
| // if lft < (oldLft + 2) // 3 -> PASS |
| // if lft > oldLft -> PASS |
| gen.addJumpIfR0LessThan(((section.lifetime + 2) / 3), |
| nextFilterLabel); |
| gen.addJumpIfR0GreaterThan(section.lifetime, nextFilterLabel); |
| } else if (section.lifetime < section.min) { |
| // Case 2a) 0 < old lft < min |
| // |
| // if lft == 0 -> PASS |
| // if lft >= min -> PASS |
| gen.addJumpIfR0Equals(0, nextFilterLabel); |
| gen.addJumpIfR0GreaterThan(section.min - 1, nextFilterLabel); |
| } else if (section.lifetime <= 3 * (long) section.min) { |
| // Case 3a) min <= old lft <= 3 * min |
| // Note that: |
| // "(old lft + 2) / 3 <= min" is equivalent to "old lft <= 3 * min" |
| // |
| // Essentially, in this range there is no "renumbering support", as the |
| // renumbering constant of 1/3 * old lft is smaller than the minimum |
| // lifetime accepted by the kernel / userspace. |
| // |
| // if lft == 0 -> PASS |
| // if lft > oldLft -> PASS |
| gen.addJumpIfR0Equals(0, nextFilterLabel); |
| gen.addJumpIfR0GreaterThan(section.lifetime, nextFilterLabel); |
| } else { |
| final String continueLabel = "Continue" + getUniqueNumberLocked(); |
| // Case 4a) otherwise |
| // |
| // if lft == 0 -> PASS |
| // if lft < min -> CONTINUE |
| // if lft < (oldLft + 2) // 3 -> PASS |
| // if lft > oldLft -> PASS |
| gen.addJumpIfR0Equals(0, nextFilterLabel); |
| gen.addJumpIfR0LessThan(section.min, continueLabel); |
| gen.addJumpIfR0LessThan(((section.lifetime + 2) / 3), |
| nextFilterLabel); |
| gen.addJumpIfR0GreaterThan(section.lifetime, nextFilterLabel); |
| |
| // CONTINUE |
| gen.defineLabel(continueLabel); |
| } |
| } |
| } |
| gen.addCountAndDrop(Counter.DROPPED_RA); |
| gen.defineLabel(nextFilterLabel); |
| } |
| } |
| |
| // TODO: Refactor these subclasses to avoid so much repetition. |
| private abstract static class KeepalivePacket { |
| // Note that the offset starts from IP header. |
| // These must be added ether header length when generating program. |
| static final int IP_HEADER_OFFSET = 0; |
| static final int IPV4_SRC_ADDR_OFFSET = IP_HEADER_OFFSET + 12; |
| |
| // Append a filter for this keepalive ack to {@code gen}. |
| // Jump to drop if it matches the keepalive ack. |
| // Jump to the next filter if packet doesn't match the keepalive ack. |
| abstract void generateFilterLocked(ApfV4GeneratorBase<?> gen) |
| throws IllegalInstructionException; |
| } |
| |
| // A class to hold NAT-T keepalive ack information. |
| private class NattKeepaliveResponse extends KeepalivePacket { |
| static final int UDP_HEADER_LEN = 8; |
| |
| protected class NattKeepaliveResponseData { |
| public final byte[] srcAddress; |
| public final int srcPort; |
| public final byte[] dstAddress; |
| public final int dstPort; |
| |
| NattKeepaliveResponseData(final NattKeepalivePacketDataParcelable sentKeepalivePacket) { |
| srcAddress = sentKeepalivePacket.dstAddress; |
| srcPort = sentKeepalivePacket.dstPort; |
| dstAddress = sentKeepalivePacket.srcAddress; |
| dstPort = sentKeepalivePacket.srcPort; |
| } |
| } |
| |
| protected final NattKeepaliveResponseData mPacket; |
| protected final byte[] mSrcDstAddr; |
| protected final byte[] mPortFingerprint; |
| // NAT-T keepalive packet |
| protected final byte[] mPayload = {(byte) 0xff}; |
| |
| NattKeepaliveResponse(final NattKeepalivePacketDataParcelable sentKeepalivePacket) { |
| mPacket = new NattKeepaliveResponseData(sentKeepalivePacket); |
| mSrcDstAddr = concatArrays(mPacket.srcAddress, mPacket.dstAddress); |
| mPortFingerprint = generatePortFingerprint(mPacket.srcPort, mPacket.dstPort); |
| } |
| |
| byte[] generatePortFingerprint(int srcPort, int dstPort) { |
| final ByteBuffer fp = ByteBuffer.allocate(4); |
| fp.order(ByteOrder.BIG_ENDIAN); |
| fp.putShort((short) srcPort); |
| fp.putShort((short) dstPort); |
| return fp.array(); |
| } |
| |
| @Override |
| @GuardedBy("ApfFilter.this") |
| void generateFilterLocked(ApfV4GeneratorBase<?> gen) throws IllegalInstructionException { |
| final String nextFilterLabel = "natt_keepalive_filter" + getUniqueNumberLocked(); |
| |
| gen.addLoadImmediate(R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET); |
| gen.addJumpIfBytesAtR0NotEqual(mSrcDstAddr, nextFilterLabel); |
| |
| // A NAT-T keepalive packet contains 1 byte payload with the value 0xff |
| // Check payload length is 1 |
| gen.addLoadFromMemory(R0, MemorySlot.IPV4_HEADER_SIZE); |
| gen.addAdd(UDP_HEADER_LEN); |
| gen.addSwap(); |
| gen.addLoad16(R0, IPV4_TOTAL_LENGTH_OFFSET); |
| gen.addNeg(R1); |
| gen.addAddR1ToR0(); |
| gen.addJumpIfR0NotEquals(1, nextFilterLabel); |
| |
| // Check that the ports match |
| gen.addLoadFromMemory(R0, MemorySlot.IPV4_HEADER_SIZE); |
| gen.addAdd(ETH_HEADER_LEN); |
| gen.addJumpIfBytesAtR0NotEqual(mPortFingerprint, nextFilterLabel); |
| |
| // Payload offset = R0 + UDP header length |
| gen.addAdd(UDP_HEADER_LEN); |
| gen.addJumpIfBytesAtR0NotEqual(mPayload, nextFilterLabel); |
| |
| gen.addCountAndDrop(Counter.DROPPED_IPV4_NATT_KEEPALIVE); |
| gen.defineLabel(nextFilterLabel); |
| } |
| |
| public String toString() { |
| try { |
| return String.format("%s -> %s", |
| ConnectivityUtils.addressAndPortToString( |
| InetAddress.getByAddress(mPacket.srcAddress), mPacket.srcPort), |
| ConnectivityUtils.addressAndPortToString( |
| InetAddress.getByAddress(mPacket.dstAddress), mPacket.dstPort)); |
| } catch (UnknownHostException e) { |
| return "Unknown host"; |
| } |
| } |
| } |
| |
| // A class to hold TCP keepalive ack information. |
| private abstract static class TcpKeepaliveAck extends KeepalivePacket { |
| protected static class TcpKeepaliveAckData { |
| public final byte[] srcAddress; |
| public final int srcPort; |
| public final byte[] dstAddress; |
| public final int dstPort; |
| public final int seq; |
| public final int ack; |
| |
| // Create the characteristics of the ack packet from the sent keepalive packet. |
| TcpKeepaliveAckData(final TcpKeepalivePacketDataParcelable sentKeepalivePacket) { |
| srcAddress = sentKeepalivePacket.dstAddress; |
| srcPort = sentKeepalivePacket.dstPort; |
| dstAddress = sentKeepalivePacket.srcAddress; |
| dstPort = sentKeepalivePacket.srcPort; |
| seq = sentKeepalivePacket.ack; |
| ack = sentKeepalivePacket.seq + 1; |
| } |
| } |
| |
| protected final TcpKeepaliveAckData mPacket; |
| protected final byte[] mSrcDstAddr; |
| protected final byte[] mPortSeqAckFingerprint; |
| |
| TcpKeepaliveAck(final TcpKeepaliveAckData packet, final byte[] srcDstAddr) { |
| mPacket = packet; |
| mSrcDstAddr = srcDstAddr; |
| mPortSeqAckFingerprint = generatePortSeqAckFingerprint(mPacket.srcPort, |
| mPacket.dstPort, mPacket.seq, mPacket.ack); |
| } |
| |
| static byte[] generatePortSeqAckFingerprint(int srcPort, int dstPort, int seq, int ack) { |
| final ByteBuffer fp = ByteBuffer.allocate(12); |
| fp.order(ByteOrder.BIG_ENDIAN); |
| fp.putShort((short) srcPort); |
| fp.putShort((short) dstPort); |
| fp.putInt(seq); |
| fp.putInt(ack); |
| return fp.array(); |
| } |
| |
| public String toString() { |
| try { |
| return String.format("%s -> %s , seq=%d, ack=%d", |
| ConnectivityUtils.addressAndPortToString( |
| InetAddress.getByAddress(mPacket.srcAddress), mPacket.srcPort), |
| ConnectivityUtils.addressAndPortToString( |
| InetAddress.getByAddress(mPacket.dstAddress), mPacket.dstPort), |
| Integer.toUnsignedLong(mPacket.seq), |
| Integer.toUnsignedLong(mPacket.ack)); |
| } catch (UnknownHostException e) { |
| return "Unknown host"; |
| } |
| } |
| |
| // Append a filter for this keepalive ack to {@code gen}. |
| // Jump to drop if it matches the keepalive ack. |
| // Jump to the next filter if packet doesn't match the keepalive ack. |
| abstract void generateFilterLocked(ApfV4GeneratorBase<?> gen) |
| throws IllegalInstructionException; |
| } |
| |
| private class TcpKeepaliveAckV4 extends TcpKeepaliveAck { |
| |
| TcpKeepaliveAckV4(final TcpKeepalivePacketDataParcelable sentKeepalivePacket) { |
| this(new TcpKeepaliveAckData(sentKeepalivePacket)); |
| } |
| TcpKeepaliveAckV4(final TcpKeepaliveAckData packet) { |
| super(packet, concatArrays(packet.srcAddress, packet.dstAddress) /* srcDstAddr */); |
| } |
| |
| @Override |
| @GuardedBy("ApfFilter.this") |
| void generateFilterLocked(ApfV4GeneratorBase<?> gen) throws IllegalInstructionException { |
| final String nextFilterLabel = "keepalive_ack" + getUniqueNumberLocked(); |
| |
| gen.addLoadImmediate(R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET); |
| gen.addJumpIfBytesAtR0NotEqual(mSrcDstAddr, nextFilterLabel); |
| |
| // Skip to the next filter if it's not zero-sized : |
| // TCP_HEADER_SIZE + IPV4_HEADER_SIZE - ipv4_total_length == 0 |
| // Load the IP header size into R1 |
| gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE); |
| // Load the TCP header size into R0 (it's indexed by R1) |
| gen.addLoad8Indexed(R0, ETH_HEADER_LEN + TCP_HEADER_SIZE_OFFSET); |
| // Size offset is in the top nibble, but it must be multiplied by 4, and the two |
| // top bits of the low nibble are guaranteed to be zeroes. Right-shift R0 by 2. |
| gen.addRightShift(2); |
| // R0 += R1 -> R0 contains TCP + IP headers length |
| gen.addAddR1ToR0(); |
| // Load IPv4 total length |
| gen.addLoad16(R1, IPV4_TOTAL_LENGTH_OFFSET); |
| gen.addNeg(R0); |
| gen.addAddR1ToR0(); |
| gen.addJumpIfR0NotEquals(0, nextFilterLabel); |
| // Add IPv4 header length |
| gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE); |
| gen.addLoadImmediate(R0, ETH_HEADER_LEN); |
| gen.addAddR1ToR0(); |
| gen.addJumpIfBytesAtR0NotEqual(mPortSeqAckFingerprint, nextFilterLabel); |
| |
| gen.addCountAndDrop(Counter.DROPPED_IPV4_KEEPALIVE_ACK); |
| gen.defineLabel(nextFilterLabel); |
| } |
| } |
| |
| private static class TcpKeepaliveAckV6 extends TcpKeepaliveAck { |
| TcpKeepaliveAckV6(final TcpKeepalivePacketDataParcelable sentKeepalivePacket) { |
| this(new TcpKeepaliveAckData(sentKeepalivePacket)); |
| } |
| TcpKeepaliveAckV6(final TcpKeepaliveAckData packet) { |
| super(packet, concatArrays(packet.srcAddress, packet.dstAddress) /* srcDstAddr */); |
| } |
| |
| @Override |
| void generateFilterLocked(ApfV4GeneratorBase<?> gen) { |
| throw new UnsupportedOperationException("IPv6 TCP Keepalive is not supported yet"); |
| } |
| } |
| |
| // Maximum number of RAs to filter for. |
| private static final int MAX_RAS = 10; |
| |
| @GuardedBy("this") |
| private final ArrayList<Ra> mRas = new ArrayList<>(); |
| @GuardedBy("this") |
| private final SparseArray<KeepalivePacket> mKeepalivePackets = new SparseArray<>(); |
| @GuardedBy("this") |
| private final List<String[]> mMdnsAllowList = new ArrayList<>(); |
| |
| // We don't want to filter an RA for it's whole lifetime as it'll be expired by the time we ever |
| // see a refresh. Using half the lifetime might be a good idea except for the fact that |
| // packets may be dropped, so let's use 6. |
| private static final int FRACTION_OF_LIFETIME_TO_FILTER = 6; |
| |
| // When did we last install a filter program? In seconds since Unix Epoch. |
| @GuardedBy("this") |
| private int mLastTimeInstalledProgram; |
| // How long should the last installed filter program live for? In seconds. |
| @GuardedBy("this") |
| private int mLastInstalledProgramMinLifetime; |
| |
| // For debugging only. The last program installed. |
| @GuardedBy("this") |
| private byte[] mLastInstalledProgram; |
| |
| /** |
| * For debugging only. Contains the latest APF buffer snapshot captured from the firmware. |
| * <p> |
| * A typical size for this buffer is 4KB. It is present only if the WiFi HAL supports |
| * IWifiStaIface#readApfPacketFilterData(), and the APF interpreter advertised support for |
| * the opcodes to access the data buffer (LDDW and STDW). |
| */ |
| @GuardedBy("this") @Nullable |
| private byte[] mDataSnapshot; |
| |
| // How many times the program was updated since we started. |
| @GuardedBy("this") |
| private int mNumProgramUpdates = 0; |
| // The maximum program size that updated since we started. |
| @GuardedBy("this") |
| private int mMaxProgramSize = 0; |
| // The maximum number of distinct RAs |
| @GuardedBy("this") |
| private int mMaxDistinctRas = 0; |
| |
| private ApfV6Generator tryToConvertToApfV6Generator(ApfV4GeneratorBase<?> gen) { |
| if (gen instanceof ApfV6Generator) { |
| return (ApfV6Generator) gen; |
| } |
| return null; |
| } |
| |
| /** |
| * Generate filter code to process ARP packets. Execution of this code ends in either the |
| * DROP_LABEL or PASS_LABEL and does not fall off the end. |
| * Preconditions: |
| * - Packet being filtered is ARP |
| */ |
| @GuardedBy("this") |
| private void generateArpFilterLocked(ApfV4GeneratorBase<?> gen) |
| throws IllegalInstructionException { |
| // Here's a basic summary of what the ARP filter program does: |
| // |
| // if clat is enabled (and we're thus IPv6-only) |
| // drop |
| // if not ARP IPv4 |
| // drop |
| // if unknown ARP opcode (ie. not reply or request) |
| // drop |
| // |
| // if ARP reply: |
| // if source ip is 0.0.0.0 |
| // drop |
| // if unicast (or multicast) |
| // pass |
| // if interface has no IPv4 address |
| // if target ip is 0.0.0.0 |
| // drop |
| // else |
| // if target ip is not the interface ip |
| // drop |
| // pass |
| // |
| // if ARP request: |
| // if interface has IPv4 address |
| // if target ip is not the interface ip |
| // drop |
| // pass |
| |
| // For IPv6 only network, drop all ARP packet. |
| if (mHasClat) { |
| gen.addCountAndDrop(Counter.DROPPED_ARP_V6_ONLY); |
| return; |
| } |
| |
| // Drop if not ARP IPv4. |
| gen.addLoadImmediate(R0, ARP_HEADER_OFFSET); |
| gen.addCountAndDropIfBytesAtR0NotEqual(ARP_IPV4_HEADER, Counter.DROPPED_ARP_NON_IPV4); |
| |
| final String checkArpRequest = "checkArpRequest"; |
| |
| gen.addLoad16(R0, ARP_OPCODE_OFFSET); |
| gen.addJumpIfR0Equals(ARP_OPCODE_REQUEST, checkArpRequest); // Skip to arp request check. |
| // Drop if unknown ARP opcode. |
| gen.addCountAndDropIfR0NotEquals(ARP_OPCODE_REPLY, Counter.DROPPED_ARP_UNKNOWN); |
| |
| /*---------- Handle ARP Replies. ----------*/ |
| |
| // Drop if ARP reply source IP is 0.0.0.0 |
| gen.addLoad32(R0, ARP_SOURCE_IP_ADDRESS_OFFSET); |
| gen.addCountAndDropIfR0Equals(IPV4_ANY_HOST_ADDRESS, Counter.DROPPED_ARP_REPLY_SPA_NO_HOST); |
| |
| // Pass if non-broadcast reply. |
| // This also accepts multicast arp, but we assume those don't exist. |
| gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET); |
| gen.addCountAndPassIfBytesAtR0NotEqual(ETHER_BROADCAST, Counter.PASSED_ARP_UNICAST_REPLY); |
| |
| // It is a broadcast reply. |
| if (mIPv4Address == null) { |
| // When there is no IPv4 address, drop GARP replies (b/29404209). |
| gen.addLoad32(R0, ARP_TARGET_IP_ADDRESS_OFFSET); |
| gen.addCountAndDropIfR0Equals(IPV4_ANY_HOST_ADDRESS, Counter.DROPPED_GARP_REPLY); |
| } else { |
| // When there is an IPv4 address, drop broadcast replies with a different target IPv4 |
| // address. |
| gen.addLoad32(R0, ARP_TARGET_IP_ADDRESS_OFFSET); |
| gen.addCountAndDropIfR0NotEquals(bytesToBEInt(mIPv4Address), |
| Counter.DROPPED_ARP_OTHER_HOST); |
| } |
| gen.addCountAndPass(Counter.PASSED_ARP_BROADCAST_REPLY); |
| |
| /*---------- Handle ARP Requests. ----------*/ |
| |
| gen.defineLabel(checkArpRequest); |
| if (mIPv4Address != null) { |
| // When there is an IPv4 address, drop unicast/broadcast requests with a different |
| // target IPv4 address. |
| gen.addLoad32(R0, ARP_TARGET_IP_ADDRESS_OFFSET); |
| gen.addCountAndDropIfR0NotEquals(bytesToBEInt(mIPv4Address), |
| Counter.DROPPED_ARP_OTHER_HOST); |
| |
| ApfV6Generator v6Gen = tryToConvertToApfV6Generator(gen); |
| if (mHardwareAddress != null && v6Gen != null) { |
| // Ethernet requires that all packets be at least 60 bytes long |
| v6Gen.addAllocate(60) |
| .addPacketCopy(ETHER_SRC_ADDR_OFFSET, ETHER_ADDR_LEN) |
| .addDataCopy(mHardwareAddress) |
| .addDataCopy(FIXED_ARP_REPLY_HEADER) |
| .addDataCopy(mHardwareAddress) |
| .addWrite32(mIPv4Address) |
| .addPacketCopy(ETHER_SRC_ADDR_OFFSET, ETHER_ADDR_LEN) |
| .addPacketCopy(ARP_SOURCE_IP_ADDRESS_OFFSET, IPV4_ADDR_LEN) |
| .addLoadFromMemory(R0, MemorySlot.TX_BUFFER_OUTPUT_POINTER) |
| .addAdd(18) |
| .addStoreToMemory(MemorySlot.TX_BUFFER_OUTPUT_POINTER, R0) |
| .addTransmitWithoutChecksum() |
| .addCountAndDrop(Counter.DROPPED_ARP_REQUEST_REPLIED); |
| } |
| } |
| // If we're not clat, and we don't have an ipv4 address, allow all ARP request to avoid |
| // racing against DHCP. |
| gen.addCountAndPass(Counter.PASSED_ARP_REQUEST); |
| } |
| |
| /** |
| * Generate filter code to process IPv4 packets. Execution of this code ends in either the |
| * DROP_LABEL or PASS_LABEL and does not fall off the end. |
| * Preconditions: |
| * - Packet being filtered is IPv4 |
| */ |
| @GuardedBy("this") |
| private void generateIPv4FilterLocked(ApfV4GeneratorBase<?> gen) |
| throws IllegalInstructionException { |
| // Here's a basic summary of what the IPv4 filter program does: |
| // |
| // if filtering multicast (i.e. multicast lock not held): |
| // if it's DHCP destined to our MAC: |
| // pass |
| // if it's L2 broadcast: |
| // drop |
| // if it's IPv4 multicast: |
| // drop |
| // if it's IPv4 broadcast: |
| // drop |
| // if keepalive ack |
| // drop |
| // pass |
| |
| if (mMulticastFilter) { |
| final String skipDhcpv4Filter = "skip_dhcp_v4_filter"; |
| |
| // Pass DHCP addressed to us. |
| // Check it's UDP. |
| gen.addLoad8(R0, IPV4_PROTOCOL_OFFSET); |
| gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipDhcpv4Filter); |
| // Check it's not a fragment or is the initial fragment. |
| gen.addLoad16(R0, IPV4_FRAGMENT_OFFSET_OFFSET); |
| gen.addJumpIfR0AnyBitsSet(IPV4_FRAGMENT_OFFSET_MASK, skipDhcpv4Filter); |
| // Check it's addressed to DHCP client port. |
| gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE); |
| gen.addLoad16Indexed(R0, TCP_UDP_DESTINATION_PORT_OFFSET); |
| gen.addJumpIfR0NotEquals(DHCP_CLIENT_PORT, skipDhcpv4Filter); |
| // Check it's DHCP to our MAC address. |
| gen.addLoadImmediate(R0, DHCP_CLIENT_MAC_OFFSET); |
| // NOTE: Relies on R1 containing IPv4 header offset. |
| gen.addAddR1ToR0(); |
| gen.addJumpIfBytesAtR0NotEqual(mHardwareAddress, skipDhcpv4Filter); |
| gen.addCountAndPass(Counter.PASSED_DHCP); |
| |
| // Drop all multicasts/broadcasts. |
| gen.defineLabel(skipDhcpv4Filter); |
| |
| // If IPv4 destination address is in multicast range, drop. |
| gen.addLoad8(R0, IPV4_DEST_ADDR_OFFSET); |
| gen.addAnd(0xf0); |
| gen.addCountAndDropIfR0Equals(0xe0, Counter.DROPPED_IPV4_MULTICAST); |
| |
| // If IPv4 broadcast packet, drop regardless of L2 (b/30231088). |
| gen.addLoad32(R0, IPV4_DEST_ADDR_OFFSET); |
| gen.addCountAndDropIfR0Equals(IPV4_BROADCAST_ADDRESS, |
| Counter.DROPPED_IPV4_BROADCAST_ADDR); |
| if (mIPv4Address != null && mIPv4PrefixLength < 31) { |
| int broadcastAddr = ipv4BroadcastAddress(mIPv4Address, mIPv4PrefixLength); |
| gen.addCountAndDropIfR0Equals(broadcastAddr, Counter.DROPPED_IPV4_BROADCAST_NET); |
| } |
| } |
| |
| // If any TCP keepalive filter matches, drop |
| generateV4KeepaliveFilters(gen); |
| |
| // If any NAT-T keepalive filter matches, drop |
| generateV4NattKeepaliveFilters(gen); |
| |
| // If TCP unicast on port 7, drop |
| generateV4TcpPort7FilterLocked(gen); |
| |
| if (mMulticastFilter) { |
| // Otherwise, this is an IPv4 unicast, pass |
| // If L2 broadcast packet, drop. |
| // TODO: can we invert this condition to fall through to the common pass case below? |
| gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET); |
| gen.addCountAndPassIfBytesAtR0NotEqual(ETHER_BROADCAST, Counter.PASSED_IPV4_UNICAST); |
| gen.addCountAndDrop(Counter.DROPPED_IPV4_L2_BROADCAST); |
| } |
| |
| // Otherwise, pass |
| gen.addCountAndPass(Counter.PASSED_IPV4); |
| } |
| |
| @GuardedBy("this") |
| private void generateKeepaliveFilters(ApfV4GeneratorBase<?> gen, Class<?> filterType, int proto, |
| int offset, String label) throws IllegalInstructionException { |
| final boolean haveKeepaliveResponses = CollectionUtils.any(mKeepalivePackets, |
| filterType::isInstance); |
| |
| // If no keepalive packets of this type |
| if (!haveKeepaliveResponses) return; |
| |
| // If not the right proto, skip keepalive filters |
| gen.addLoad8(R0, offset); |
| gen.addJumpIfR0NotEquals(proto, label); |
| |
| // Drop Keepalive responses |
| for (int i = 0; i < mKeepalivePackets.size(); ++i) { |
| final KeepalivePacket response = mKeepalivePackets.valueAt(i); |
| if (filterType.isInstance(response)) response.generateFilterLocked(gen); |
| } |
| |
| gen.defineLabel(label); |
| } |
| |
| @GuardedBy("this") |
| private void generateV4KeepaliveFilters(ApfV4GeneratorBase<?> gen) |
| throws IllegalInstructionException { |
| generateKeepaliveFilters(gen, TcpKeepaliveAckV4.class, IPPROTO_TCP, IPV4_PROTOCOL_OFFSET, |
| "skip_v4_keepalive_filter"); |
| } |
| |
| @GuardedBy("this") |
| private void generateV4NattKeepaliveFilters(ApfV4GeneratorBase<?> gen) |
| throws IllegalInstructionException { |
| generateKeepaliveFilters(gen, NattKeepaliveResponse.class, |
| IPPROTO_UDP, IPV4_PROTOCOL_OFFSET, "skip_v4_nattkeepalive_filter"); |
| } |
| |
| /** |
| * Generate filter code to process IPv6 packets. Execution of this code ends in either the |
| * DROP_LABEL or PASS_LABEL, or falls off the end for ICMPv6 packets. |
| * Preconditions: |
| * - Packet being filtered is IPv6 |
| */ |
| @GuardedBy("this") |
| private void generateIPv6FilterLocked(ApfV4GeneratorBase<?> gen) |
| throws IllegalInstructionException { |
| // Here's a basic summary of what the IPv6 filter program does: |
| // |
| // if there is a hop-by-hop option present (e.g. MLD query) |
| // pass |
| // if we're dropping multicast |
| // if it's not IPCMv6 or it's ICMPv6 but we're in doze mode: |
| // if it's multicast: |
| // drop |
| // pass |
| // if it's ICMPv6 RS to any: |
| // drop |
| // if it's ICMPv6 NA to anything in ff02::/120 |
| // drop |
| // if keepalive ack |
| // drop |
| |
| gen.addLoad8(R0, IPV6_NEXT_HEADER_OFFSET); |
| |
| // MLD packets set the router-alert hop-by-hop option. |
| // TODO: be smarter about not blindly passing every packet with HBH options. |
| gen.addCountAndPassIfR0Equals(IPPROTO_HOPOPTS, Counter.PASSED_MLD); |
| |
| // Drop multicast if the multicast filter is enabled. |
| if (mMulticastFilter) { |
| final String skipIPv6MulticastFilterLabel = "skipIPv6MulticastFilter"; |
| final String dropAllIPv6MulticastsLabel = "dropAllIPv6Multicast"; |
| |
| // While in doze mode, drop ICMPv6 multicast pings, let the others pass. |
| // While awake, let all ICMPv6 multicasts through. |
| if (mInDozeMode) { |
| // Not ICMPv6? -> Proceed to multicast filtering |
| gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, dropAllIPv6MulticastsLabel); |
| |
| // ICMPv6 but not ECHO? -> Skip the multicast filter. |
| // (ICMPv6 ECHO requests will go through the multicast filter below). |
| gen.addLoad8(R0, ICMP6_TYPE_OFFSET); |
| gen.addJumpIfR0NotEquals(ICMPV6_ECHO_REQUEST_TYPE, skipIPv6MulticastFilterLabel); |
| } else { |
| gen.addJumpIfR0Equals(IPPROTO_ICMPV6, skipIPv6MulticastFilterLabel); |
| } |
| |
| // Drop all other packets sent to ff00::/8 (multicast prefix). |
| gen.defineLabel(dropAllIPv6MulticastsLabel); |
| gen.addLoad8(R0, IPV6_DEST_ADDR_OFFSET); |
| gen.addCountAndDropIfR0Equals(0xff, Counter.DROPPED_IPV6_NON_ICMP_MULTICAST); |
| // If any keepalive filter matches, drop |
| generateV6KeepaliveFilters(gen); |
| // Not multicast. Pass. |
| gen.addCountAndPass(Counter.PASSED_IPV6_UNICAST_NON_ICMP); |
| gen.defineLabel(skipIPv6MulticastFilterLabel); |
| } else { |
| generateV6KeepaliveFilters(gen); |
| // If not ICMPv6, pass. |
| gen.addCountAndPassIfR0NotEquals(IPPROTO_ICMPV6, Counter.PASSED_IPV6_NON_ICMP); |
| } |
| |
| // If we got this far, the packet is ICMPv6. Drop some specific types. |
| |
| // Add unsolicited multicast neighbor announcements filter |
| String skipUnsolicitedMulticastNALabel = "skipUnsolicitedMulticastNA"; |
| gen.addLoad8(R0, ICMP6_TYPE_OFFSET); |
| // Drop all router solicitations (b/32833400) |
| gen.addCountAndDropIfR0Equals(ICMPV6_ROUTER_SOLICITATION, |
| Counter.DROPPED_IPV6_ROUTER_SOLICITATION); |
| // If not neighbor announcements, skip filter. |
| gen.addJumpIfR0NotEquals(ICMPV6_NEIGHBOR_ADVERTISEMENT, skipUnsolicitedMulticastNALabel); |
| // Drop all multicast NA to ff02::/120. |
| // This is a way to cover ff02::1 and ff02::2 with a single JNEBS. |
| // TODO: Drop only if they don't contain the address of on-link neighbours. |
| final byte[] unsolicitedNaDropPrefix = Arrays.copyOf(IPV6_ALL_NODES_ADDRESS, 15); |
| gen.addLoadImmediate(R0, IPV6_DEST_ADDR_OFFSET); |
| gen.addJumpIfBytesAtR0NotEqual(unsolicitedNaDropPrefix, skipUnsolicitedMulticastNALabel); |
| |
| gen.addCountAndDrop(Counter.DROPPED_IPV6_MULTICAST_NA); |
| gen.defineLabel(skipUnsolicitedMulticastNALabel); |
| } |
| |
| /** Encodes qname in TLV pattern. */ |
| @VisibleForTesting |
| public static byte[] encodeQname(String[] labels) { |
| final ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| for (String label : labels) { |
| byte[] labelBytes = label.getBytes(StandardCharsets.UTF_8); |
| out.write(labelBytes.length); |
| out.write(labelBytes, 0, labelBytes.length); |
| } |
| out.write(0); |
| return out.toByteArray(); |
| } |
| |
| /** |
| * Generate filter code to process mDNS packets. Execution of this code ends in * DROP_LABEL |
| * or PASS_LABEL if the packet is mDNS packets. Otherwise, skip this check. |
| */ |
| @GuardedBy("this") |
| private void generateMdnsFilterLocked(ApfV4GeneratorBase<?> gen) |
| throws IllegalInstructionException { |
| final String skipMdnsv4Filter = "skip_mdns_v4_filter"; |
| final String skipMdnsFilter = "skip_mdns_filter"; |
| final String checkMdnsUdpPort = "check_mdns_udp_port"; |
| final String mDnsAcceptPacket = "mdns_accept_packet"; |
| final String mDnsDropPacket = "mdns_drop_packet"; |
| |
| // Only turn on the filter if multicast filter is on and the qname allowlist is non-empty. |
| if (!mMulticastFilter || mMdnsAllowList.isEmpty()) { |
| return; |
| } |
| |
| // Here's a basic summary of what the mDNS filter program does: |
| // |
| // if it is a multicast mDNS packet |
| // if QDCOUNT != 1 |
| // pass |
| // else if the QNAME is in the allowlist |
| // pass |
| // else: |
| // drop |
| // |
| // A packet is considered as a multicast mDNS packet if it matches all the following |
| // conditions |
| // 1. its destination MAC address matches 01:00:5E:00:00:FB or 33:33:00:00:00:FB, for |
| // v4 and v6 respectively. |
| // 2. it is an IPv4/IPv6 packet |
| // 3. it is a UDP packet with port 5353 |
| |
| // Check it's L2 mDNS multicast address. |
| gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET); |
| gen.addJumpIfBytesAtR0NotEqual(ETH_MULTICAST_MDNS_V4_MAC_ADDRESS, skipMdnsv4Filter); |
| |
| // Checks it's IPv4. |
| gen.addLoad16(R0, ETH_ETHERTYPE_OFFSET); |
| gen.addJumpIfR0NotEquals(ETH_P_IP, skipMdnsFilter); |
| |
| // Check it's not a fragment. |
| gen.addLoad16(R0, IPV4_FRAGMENT_OFFSET_OFFSET); |
| gen.addJumpIfR0AnyBitsSet(IPV4_FRAGMENT_MORE_FRAGS_MASK | IPV4_FRAGMENT_OFFSET_MASK, |
| skipMdnsFilter); |
| |
| // Checks it's UDP. |
| gen.addLoad8(R0, IPV4_PROTOCOL_OFFSET); |
| gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipMdnsFilter); |
| |
| // Set R1 to IPv4 header. |
| gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE); |
| gen.addJump(checkMdnsUdpPort); |
| |
| gen.defineLabel(skipMdnsv4Filter); |
| |
| // Checks it's L2 mDNS multicast address. |
| // Relies on R0 containing the ethernet destination mac address offset. |
| gen.addJumpIfBytesAtR0NotEqual(ETH_MULTICAST_MDNS_V6_MAC_ADDRESS, skipMdnsFilter); |
| |
| // Checks it's IPv6. |
| gen.addLoad16(R0, ETH_ETHERTYPE_OFFSET); |
| gen.addJumpIfR0NotEquals(ETH_P_IPV6, skipMdnsFilter); |
| |
| // Checks it's UDP. |
| gen.addLoad8(R0, IPV6_NEXT_HEADER_OFFSET); |
| gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipMdnsFilter); |
| |
| // Set R1 to IPv6 header. |
| gen.addLoadImmediate(R1, IPV6_HEADER_LEN); |
| |
| // Checks it's mDNS UDP port |
| gen.defineLabel(checkMdnsUdpPort); |
| gen.addLoad16Indexed(R0, TCP_UDP_DESTINATION_PORT_OFFSET); |
| gen.addJumpIfR0NotEquals(MDNS_PORT, skipMdnsFilter); |
| |
| gen.addLoad16Indexed(R0, MDNS_QDCOUNT_OFFSET); |
| // If QDCOUNT != 1, pass the packet |
| gen.addJumpIfR0NotEquals(1, mDnsAcceptPacket); |
| |
| // If QDCOUNT == 1, matches the QNAME with allowlist. |
| // Load offset for the first QNAME. |
| gen.addLoadImmediate(R0, MDNS_QNAME_OFFSET); |
| gen.addAddR1ToR0(); |
| |
| // Check first QNAME against allowlist |
| for (int i = 0; i < mMdnsAllowList.size(); ++i) { |
| final String mDnsNextAllowedQnameCheck = "mdns_next_allowed_qname_check" + i; |
| final byte[] encodedQname = encodeQname(mMdnsAllowList.get(i)); |
| gen.addJumpIfBytesAtR0NotEqual(encodedQname, mDnsNextAllowedQnameCheck); |
| // QNAME matched |
| gen.addJump(mDnsAcceptPacket); |
| // QNAME not matched |
| gen.defineLabel(mDnsNextAllowedQnameCheck); |
| } |
| // If QNAME doesn't match any entries in allowlist, drop the packet. |
| gen.defineLabel(mDnsDropPacket); |
| gen.addCountAndDrop(Counter.DROPPED_MDNS); |
| |
| gen.defineLabel(mDnsAcceptPacket); |
| gen.addCountAndPass(Counter.PASSED_MDNS); |
| |
| |
| gen.defineLabel(skipMdnsFilter); |
| } |
| |
| /** |
| * Generate filter code to drop IPv4 TCP packets on port 7. |
| * <p> |
| * On entry, we know it is IPv4 ethertype, but don't know anything else. |
| * R0/R1 have nothing useful in them, and can be clobbered. |
| */ |
| @GuardedBy("this") |
| private void generateV4TcpPort7FilterLocked(ApfV4GeneratorBase<?> gen) |
| throws IllegalInstructionException { |
| final String skipPort7V4Filter = "skip_port7_v4_filter"; |
| |
| // Check it's TCP. |
| gen.addLoad8(R0, IPV4_PROTOCOL_OFFSET); |
| gen.addJumpIfR0NotEquals(IPPROTO_TCP, skipPort7V4Filter); |
| |
| // Check it's not a fragment or is the initial fragment. |
| gen.addLoad16(R0, IPV4_FRAGMENT_OFFSET_OFFSET); |
| gen.addJumpIfR0AnyBitsSet(IPV4_FRAGMENT_OFFSET_MASK, skipPort7V4Filter); |
| |
| // Check it's destination port 7. |
| gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE); |
| gen.addLoad16Indexed(R0, TCP_UDP_DESTINATION_PORT_OFFSET); |
| gen.addJumpIfR0NotEquals(ECHO_PORT, skipPort7V4Filter); |
| |
| // Drop it. |
| gen.addCountAndDrop(Counter.DROPPED_IPV4_TCP_PORT7_UNICAST); |
| |
| // Skip label. |
| gen.defineLabel(skipPort7V4Filter); |
| } |
| |
| @GuardedBy("this") |
| private void generateV6KeepaliveFilters(ApfV4GeneratorBase<?> gen) |
| throws IllegalInstructionException { |
| generateKeepaliveFilters(gen, TcpKeepaliveAckV6.class, IPPROTO_TCP, IPV6_NEXT_HEADER_OFFSET, |
| "skip_v6_keepalive_filter"); |
| } |
| |
| /** |
| * Begin generating an APF program to: |
| * <ul> |
| * <li>Drop/Pass 802.3 frames (based on policy) |
| * <li>Drop packets with EtherType within the Black List |
| * <li>Drop ARP requests not for us, if mIPv4Address is set, |
| * <li>Drop IPv4 broadcast packets, except DHCP destined to our MAC, |
| * <li>Drop IPv4 multicast packets, if mMulticastFilter, |
| * <li>Pass all other IPv4 packets, |
| * <li>Drop all broadcast non-IP non-ARP packets. |
| * <li>Pass all non-ICMPv6 IPv6 packets, |
| * <li>Pass all non-IPv4 and non-IPv6 packets, |
| * <li>Drop IPv6 ICMPv6 NAs to anything in ff02::/120. |
| * <li>Drop IPv6 ICMPv6 RSs. |
| * <li>Filter IPv4 packets (see generateIPv4FilterLocked()) |
| * <li>Filter IPv6 packets (see generateIPv6FilterLocked()) |
| * <li>Let execution continue off the end of the program for IPv6 ICMPv6 packets. This allows |
| * insertion of RA filters here, or if there aren't any, just passes the packets. |
| * </ul> |
| */ |
| @GuardedBy("this") |
| @VisibleForTesting |
| protected ApfV4GeneratorBase<?> emitPrologueLocked() throws IllegalInstructionException { |
| // This is guaranteed to succeed because of the check in maybeCreate. |
| ApfV4GeneratorBase<?> gen; |
| if (SdkLevel.isAtLeastV() |
| && ApfV6Generator.supportsVersion(mApfCapabilities.apfVersionSupported)) { |
| gen = new ApfV6Generator(mApfCapabilities.maximumApfProgramSize); |
| } else { |
| gen = new ApfV4Generator(mApfCapabilities.apfVersionSupported); |
| } |
| |
| if (mApfCapabilities.hasDataAccess()) { |
| if (gen instanceof ApfV4Generator) { |
| // Increment TOTAL_PACKETS. |
| // Only needed in APFv4. |
| // In APFv6, the interpreter will increase the counter on packet receive. |
| gen.addIncrementCounter(Counter.TOTAL_PACKETS); |
| } |
| |
| gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_SECONDS); |
| gen.addStoreCounter(Counter.FILTER_AGE_SECONDS, R0); |
| |
| // requires a new enough APFv5+ interpreter, otherwise will be 0 |
| gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_16384THS); |
| gen.addStoreCounter(Counter.FILTER_AGE_16384THS, R0); |
| |
| // requires a new enough APFv5+ interpreter, otherwise will be 0 |
| gen.addLoadFromMemory(R0, MemorySlot.APF_VERSION); |
| gen.addStoreCounter(Counter.APF_VERSION, R0); |
| |
| // store this program's sequential id, for later comparison |
| gen.addLoadImmediate(R0, mNumProgramUpdates); |
| gen.addStoreCounter(Counter.APF_PROGRAM_ID, R0); |
| } |
| |
| // Here's a basic summary of what the initial program does: |
| // |
| // if it's a 802.3 Frame (ethtype < 0x0600): |
| // drop or pass based on configurations |
| // if it has a ether-type that belongs to the black list |
| // drop |
| // if it's ARP: |
| // insert ARP filter to drop or pass these appropriately |
| // if it's IPv4: |
| // insert IPv4 filter to drop or pass these appropriately |
| // if it's not IPv6: |
| // if it's broadcast: |
| // drop |
| // pass |
| // insert IPv6 filter to drop, pass, or fall off the end for ICMPv6 packets |
| |
| gen.addLoad16(R0, ETH_ETHERTYPE_OFFSET); |
| |
| if (mDrop802_3Frames) { |
| // drop 802.3 frames (ethtype < 0x0600) |
| gen.addCountAndDropIfR0LessThan(ETH_TYPE_MIN, Counter.DROPPED_802_3_FRAME); |
| } |
| |
| // Handle ether-type black list |
| for (int p : mEthTypeBlackList) { |
| // TODO: Refactorings increased APFv4 code size; optimize for reduction. |
| gen.addCountAndDropIfR0Equals(p, Counter.DROPPED_ETHERTYPE_DENYLISTED); |
| } |
| |
| // Add ARP filters: |
| String skipArpFiltersLabel = "skipArpFilters"; |
| gen.addJumpIfR0NotEquals(ETH_P_ARP, skipArpFiltersLabel); |
| generateArpFilterLocked(gen); |
| gen.defineLabel(skipArpFiltersLabel); |
| |
| // Add mDNS filter: |
| generateMdnsFilterLocked(gen); |
| gen.addLoad16(R0, ETH_ETHERTYPE_OFFSET); |
| |
| // Add IPv4 filters: |
| String skipIPv4FiltersLabel = "skipIPv4Filters"; |
| gen.addJumpIfR0NotEquals(ETH_P_IP, skipIPv4FiltersLabel); |
| generateIPv4FilterLocked(gen); |
| gen.defineLabel(skipIPv4FiltersLabel); |
| |
| // Check for IPv6: |
| // NOTE: Relies on R0 containing ethertype. This is safe because if we got here, we did |
| // not execute the IPv4 filter, since this filter do not fall through, but either drop or |
| // pass. |
| String ipv6FilterLabel = "IPv6Filters"; |
| gen.addJumpIfR0Equals(ETH_P_IPV6, ipv6FilterLabel); |
| |
| // Drop non-IP non-ARP broadcasts, pass the rest |
| gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET); |
| gen.addCountAndPassIfBytesAtR0NotEqual(ETHER_BROADCAST, Counter.PASSED_NON_IP_UNICAST); |
| gen.addCountAndDrop(Counter.DROPPED_ETH_BROADCAST); |
| |
| // Add IPv6 filters: |
| gen.defineLabel(ipv6FilterLabel); |
| generateIPv6FilterLocked(gen); |
| return gen; |
| } |
| |
| /** |
| * Append packet counting epilogue to the APF program. |
| * <p> |
| * Currently, the epilogue consists of two trampolines which count passed and dropped packets |
| * before jumping to the actual PASS and DROP labels. |
| */ |
| @GuardedBy("this") |
| private void emitEpilogue(ApfV4GeneratorBase<?> gen) throws IllegalInstructionException { |
| // Execution will reach here if none of the filters match, which will pass the packet to |
| // the application processor. |
| gen.addCountAndPass(Counter.PASSED_IPV6_ICMP); |
| |
| // TODO: merge the addCountTrampoline() into generate() method |
| gen.addCountTrampoline(); |
| } |
| |
| /** |
| * Generate and install a new filter program. |
| */ |
| @GuardedBy("this") |
| @SuppressWarnings("GuardedBy") // errorprone false positive on ra#generateFilterLocked |
| @VisibleForTesting |
| public void installNewProgramLocked() { |
| ArrayList<Ra> rasToFilter = new ArrayList<>(); |
| final byte[] program; |
| int programMinLft = Integer.MAX_VALUE; |
| int maximumApfProgramSize = mApfCapabilities.maximumApfProgramSize; |
| if (mApfCapabilities.hasDataAccess()) { |
| // Reserve space for the counters. |
| maximumApfProgramSize -= Counter.totalSize(); |
| } |
| |
| // Prevent generating (and thus installing) larger programs |
| if (maximumApfProgramSize > mInstallableProgramSizeClamp) { |
| maximumApfProgramSize = mInstallableProgramSizeClamp; |
| } |
| |
| // Ensure the entire APF program uses the same time base. |
| int timeSeconds = secondsSinceBoot(); |
| try { |
| // Step 1: Determine how many RA filters we can fit in the program. |
| ApfV4GeneratorBase<?> gen = emitPrologueLocked(); |
| |
| // The epilogue normally goes after the RA filters, but add it early to include its |
| // length when estimating the total. |
| emitEpilogue(gen); |
| |
| // Can't fit the program even without any RA filters? |
| if (gen.programLengthOverEstimate() > maximumApfProgramSize) { |
| Log.e(TAG, "Program exceeds maximum size " + maximumApfProgramSize); |
| sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE); |
| return; |
| } |
| |
| for (Ra ra : mRas) { |
| // skip filter if it has expired. |
| if (ra.getRemainingFilterLft(timeSeconds) <= 0) continue; |
| ra.generateFilterLocked(gen, timeSeconds); |
| // Stop if we get too big. |
| if (gen.programLengthOverEstimate() > maximumApfProgramSize) { |
| if (VDBG) Log.d(TAG, "Past maximum program size, skipping RAs"); |
| sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE); |
| break; |
| } |
| |
| rasToFilter.add(ra); |
| } |
| |
| // Step 2: Actually generate the program |
| gen = emitPrologueLocked(); |
| for (Ra ra : rasToFilter) { |
| ra.generateFilterLocked(gen, timeSeconds); |
| programMinLft = Math.min(programMinLft, ra.getRemainingFilterLft(timeSeconds)); |
| } |
| emitEpilogue(gen); |
| program = gen.generate(); |
| } catch (IllegalInstructionException | IllegalStateException | IllegalArgumentException e) { |
| Log.e(TAG, "Failed to generate APF program.", e); |
| sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_GENERATE_FILTER_EXCEPTION); |
| return; |
| } |
| if (mIsRunning) { |
| // Update data snapshot every time we install a new program |
| mIpClientCallback.startReadPacketFilter("new program install"); |
| if (!mIpClientCallback.installPacketFilter(program)) { |
| sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE); |
| } |
| } |
| mLastTimeInstalledProgram = timeSeconds; |
| mLastInstalledProgramMinLifetime = programMinLft; |
| mLastInstalledProgram = program; |
| mNumProgramUpdates++; |
| mMaxProgramSize = Math.max(mMaxProgramSize, program.length); |
| |
| if (VDBG) { |
| hexDump("Installing filter: ", program, program.length); |
| } |
| } |
| |
| private void hexDump(String msg, byte[] packet, int length) { |
| log(msg + HexDump.toHexString(packet, 0, length, false /* lowercase */)); |
| } |
| |
| // Get the minimum value excludes zero. This is used for calculating the lowest lifetime values |
| // in RA packets. Zero lifetimes are excluded because we want to detect whether there is any |
| // unusually small lifetimes but zero lifetime is actually valid (cease to be a default router |
| // or the option is no longer be used). Number of zero lifetime RAs is collected in a different |
| // Metrics. |
| private long getMinForPositiveValue(long oldMinValue, long value) { |
| if (value < 1) return oldMinValue; |
| return Math.min(oldMinValue, value); |
| } |
| |
| private int getMinForPositiveValue(int oldMinValue, int value) { |
| return (int) getMinForPositiveValue((long) oldMinValue, (long) value); |
| } |
| |
| /** |
| * Process an RA packet, updating the list of known RAs and installing a new APF program |
| * if the current APF program should be updated. |
| */ |
| @VisibleForTesting |
| public synchronized void processRa(byte[] packet, int length) { |
| if (VDBG) hexDump("Read packet = ", packet, length); |
| |
| final Ra ra; |
| try { |
| ra = new Ra(packet, length); |
| } catch (Exception e) { |
| Log.e(TAG, "Error parsing RA", e); |
| mNumParseErrorRas++; |
| return; |
| } |
| |
| // Update info for Metrics |
| mLowestRouterLifetimeSeconds = getMinForPositiveValue( |
| mLowestRouterLifetimeSeconds, ra.routerLifetime()); |
| mLowestPioValidLifetimeSeconds = getMinForPositiveValue( |
| mLowestPioValidLifetimeSeconds, ra.minPioValidLifetime()); |
| mLowestRioRouteLifetimeSeconds = getMinForPositiveValue( |
| mLowestRioRouteLifetimeSeconds, ra.minRioRouteLifetime()); |
| mLowestRdnssLifetimeSeconds = getMinForPositiveValue( |
| mLowestRdnssLifetimeSeconds, ra.minRdnssLifetime()); |
| |
| // Remove all expired RA filters before trying to match the new RA. |
| // TODO: matches() still checks that the old RA filter has not expired. Consider removing |
| // that check. |
| final int now = secondsSinceBoot(); |
| mRas.removeIf(item -> item.getRemainingFilterLft(now) <= 0); |
| |
| // Have we seen this RA before? |
| for (int i = 0; i < mRas.size(); i++) { |
| final Ra oldRa = mRas.get(i); |
| final Ra.MatchType result = oldRa.matches(ra); |
| if (result == Ra.MatchType.MATCH_PASS) { |
| log("Updating RA from " + oldRa + " to " + ra); |
| |
| // Keep mRas in LRU order so as to prioritize generating filters for recently seen |
| // RAs. LRU prioritizes this because RA filters are generated in order from mRas |
| // until the filter program exceeds the maximum filter program size allowed by the |
| // chipset, so RAs appearing earlier in mRas are more likely to make it into the |
| // filter program. |
| // TODO: consider sorting the RAs in order of increasing expiry time as well. |
| // Swap to front of array. |
| mRas.remove(i); |
| mRas.add(0, ra); |
| |
| // Rate limit program installation |
| if (mTokenBucket.get()) { |
| installNewProgramLocked(); |
| } else { |
| Log.e(TAG, "Failed to install prog for tracked RA, too many updates. " + ra); |
| } |
| return; |
| } else if (result == Ra.MatchType.MATCH_DROP) { |
| log("Ignoring RA " + ra + " which matches " + oldRa); |
| return; |
| } |
| } |
| mMaxDistinctRas = Math.max(mMaxDistinctRas, mRas.size() + 1); |
| if (mRas.size() >= MAX_RAS) { |
| // Remove the last (i.e. oldest) RA. |
| mRas.remove(mRas.size() - 1); |
| } |
| log("Adding " + ra); |
| mRas.add(0, ra); |
| // Rate limit program installation |
| if (mTokenBucket.get()) { |
| installNewProgramLocked(); |
| } else { |
| Log.e(TAG, "Failed to install prog for new RA, too many updates. " + ra); |
| } |
| } |
| |
| /** |
| * Create an {@link ApfFilter} if {@code apfCapabilities} indicates support for packet |
| * filtering using APF programs. |
| */ |
| public static ApfFilter maybeCreate(Context context, ApfConfiguration config, |
| InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback, |
| NetworkQuirkMetrics networkQuirkMetrics) { |
| if (context == null || config == null || ifParams == null) return null; |
| ApfCapabilities apfCapabilities = config.apfCapabilities; |
| if (apfCapabilities == null) return null; |
| if (apfCapabilities.apfVersionSupported < 2) return null; |
| if (apfCapabilities.maximumApfProgramSize < 512) { |
| Log.e(TAG, "Unacceptably small APF limit: " + apfCapabilities.maximumApfProgramSize); |
| return null; |
| } |
| // For now only support generating programs for Ethernet frames. If this restriction is |
| // lifted the program generator will need its offsets adjusted. |
| if (apfCapabilities.apfPacketFormat != ARPHRD_ETHER) return null; |
| if (!ApfV4Generator.supportsVersion(apfCapabilities.apfVersionSupported)) { |
| Log.e(TAG, "Unsupported APF version: " + apfCapabilities.apfVersionSupported); |
| return null; |
| } |
| |
| return new ApfFilter(context, config, ifParams, ipClientCallback, networkQuirkMetrics); |
| } |
| |
| private synchronized void collectAndSendMetrics() { |
| if (mIpClientRaInfoMetrics == null || mApfSessionInfoMetrics == null) return; |
| final long sessionDurationMs = mClock.elapsedRealtime() - mSessionStartMs; |
| if (sessionDurationMs < mMinMetricsSessionDurationMs) return; |
| |
| // Collect and send IpClientRaInfoMetrics. |
| mIpClientRaInfoMetrics.setMaxNumberOfDistinctRas(mMaxDistinctRas); |
| mIpClientRaInfoMetrics.setNumberOfZeroLifetimeRas(mNumZeroLifetimeRas); |
| mIpClientRaInfoMetrics.setNumberOfParsingErrorRas(mNumParseErrorRas); |
| mIpClientRaInfoMetrics.setLowestRouterLifetimeSeconds(mLowestRouterLifetimeSeconds); |
| mIpClientRaInfoMetrics.setLowestPioValidLifetimeSeconds(mLowestPioValidLifetimeSeconds); |
| mIpClientRaInfoMetrics.setLowestRioRouteLifetimeSeconds(mLowestRioRouteLifetimeSeconds); |
| mIpClientRaInfoMetrics.setLowestRdnssLifetimeSeconds(mLowestRdnssLifetimeSeconds); |
| mIpClientRaInfoMetrics.statsWrite(); |
| |
| // Collect and send ApfSessionInfoMetrics. |
| mApfSessionInfoMetrics.setVersion(mApfCapabilities.apfVersionSupported); |
| mApfSessionInfoMetrics.setMemorySize(mApfCapabilities.maximumApfProgramSize); |
| mApfSessionInfoMetrics.setApfSessionDurationSeconds( |
| (int) (sessionDurationMs / DateUtils.SECOND_IN_MILLIS)); |
| mApfSessionInfoMetrics.setNumOfTimesApfProgramUpdated(mNumProgramUpdates); |
| mApfSessionInfoMetrics.setMaxProgramSize(mMaxProgramSize); |
| for (Map.Entry<Counter, Long> entry : mApfCounterTracker.getCounters().entrySet()) { |
| if (entry.getValue() > 0) { |
| mApfSessionInfoMetrics.addApfCounter(entry.getKey(), entry.getValue()); |
| } |
| } |
| mApfSessionInfoMetrics.statsWrite(); |
| } |
| |
| public synchronized void shutdown() { |
| collectAndSendMetrics(); |
| if (mReceiveThread != null) { |
| log("shutting down"); |
| mReceiveThread.halt(); // Also closes socket. |
| mReceiveThread = null; |
| } |
| mRas.clear(); |
| mDependencies.removeBroadcastReceiver(mDeviceIdleReceiver); |
| } |
| |
| public synchronized void setMulticastFilter(boolean isEnabled) { |
| if (mMulticastFilter == isEnabled) return; |
| mMulticastFilter = isEnabled; |
| installNewProgramLocked(); |
| } |
| |
| /** Adds qname to the mDNS allowlist */ |
| public synchronized void addToMdnsAllowList(String[] labels) { |
| mMdnsAllowList.add(labels); |
| if (mMulticastFilter) { |
| installNewProgramLocked(); |
| } |
| } |
| |
| /** Removes qname from the mDNS allowlist */ |
| public synchronized void removeFromAllowList(String[] labels) { |
| mMdnsAllowList.removeIf(e -> Arrays.equals(labels, e)); |
| if (mMulticastFilter) { |
| installNewProgramLocked(); |
| } |
| } |
| |
| @VisibleForTesting |
| public synchronized void setDozeMode(boolean isEnabled) { |
| if (mInDozeMode == isEnabled) return; |
| mInDozeMode = isEnabled; |
| installNewProgramLocked(); |
| } |
| |
| @VisibleForTesting |
| public synchronized boolean isInDozeMode() { |
| return mInDozeMode; |
| } |
| |
| /** Find the single IPv4 LinkAddress if there is one, otherwise return null. */ |
| private static LinkAddress findIPv4LinkAddress(LinkProperties lp) { |
| LinkAddress ipv4Address = null; |
| for (LinkAddress address : lp.getLinkAddresses()) { |
| if (!(address.getAddress() instanceof Inet4Address)) { |
| continue; |
| } |
| if (ipv4Address != null && !ipv4Address.isSameAddressAs(address)) { |
| // More than one IPv4 address, abort. |
| return null; |
| } |
| ipv4Address = address; |
| } |
| return ipv4Address; |
| } |
| |
| public synchronized void setLinkProperties(LinkProperties lp) { |
| // NOTE: Do not keep a copy of LinkProperties as it would further duplicate state. |
| final LinkAddress ipv4Address = findIPv4LinkAddress(lp); |
| final byte[] addr = (ipv4Address != null) ? ipv4Address.getAddress().getAddress() : null; |
| final int prefix = (ipv4Address != null) ? ipv4Address.getPrefixLength() : 0; |
| if ((prefix == mIPv4PrefixLength) && Arrays.equals(addr, mIPv4Address)) { |
| return; |
| } |
| mIPv4Address = addr; |
| mIPv4PrefixLength = prefix; |
| installNewProgramLocked(); |
| } |
| |
| @Override |
| public synchronized void updateClatInterfaceState(boolean add) { |
| if (mHasClat == add) { |
| return; |
| } |
| mHasClat = add; |
| installNewProgramLocked(); |
| } |
| |
| /** |
| * Add TCP keepalive ack packet filter. |
| * This will add a filter to drop acks to the keepalive packet passed as an argument. |
| * |
| * @param slot The index used to access the filter. |
| * @param sentKeepalivePacket The attributes of the sent keepalive packet. |
| */ |
| public synchronized void addTcpKeepalivePacketFilter(final int slot, |
| final TcpKeepalivePacketDataParcelable sentKeepalivePacket) { |
| log("Adding keepalive ack(" + slot + ")"); |
| if (null != mKeepalivePackets.get(slot)) { |
| throw new IllegalArgumentException("Keepalive slot " + slot + " is occupied"); |
| } |
| final int ipVersion = sentKeepalivePacket.srcAddress.length == 4 ? 4 : 6; |
| mKeepalivePackets.put(slot, (ipVersion == 4) |
| ? new TcpKeepaliveAckV4(sentKeepalivePacket) |
| : new TcpKeepaliveAckV6(sentKeepalivePacket)); |
| installNewProgramLocked(); |
| } |
| |
| /** |
| * Add NAT-T keepalive packet filter. |
| * This will add a filter to drop NAT-T keepalive packet which is passed as an argument. |
| * |
| * @param slot The index used to access the filter. |
| * @param sentKeepalivePacket The attributes of the sent keepalive packet. |
| */ |
| public synchronized void addNattKeepalivePacketFilter(final int slot, |
| final NattKeepalivePacketDataParcelable sentKeepalivePacket) { |
| log("Adding NAT-T keepalive packet(" + slot + ")"); |
| if (null != mKeepalivePackets.get(slot)) { |
| throw new IllegalArgumentException("NAT-T Keepalive slot " + slot + " is occupied"); |
| } |
| |
| // TODO : update ApfFilter to support dropping v6 keepalives |
| if (sentKeepalivePacket.srcAddress.length != 4) { |
| return; |
| } |
| |
| mKeepalivePackets.put(slot, new NattKeepaliveResponse(sentKeepalivePacket)); |
| installNewProgramLocked(); |
| } |
| |
| /** |
| * Remove keepalive packet filter. |
| * |
| * @param slot The index used to access the filter. |
| */ |
| public synchronized void removeKeepalivePacketFilter(int slot) { |
| log("Removing keepalive packet(" + slot + ")"); |
| mKeepalivePackets.remove(slot); |
| installNewProgramLocked(); |
| } |
| |
| public synchronized void dump(IndentingPrintWriter pw) { |
| pw.println("Capabilities: " + mApfCapabilities); |
| pw.println("InstallableProgramSizeClamp: " + mInstallableProgramSizeClamp); |
| pw.println("Filter update status: " + (mIsRunning ? "RUNNING" : "PAUSED")); |
| pw.println("Receive thread: " + (mReceiveThread != null ? "RUNNING" : "STOPPED")); |
| pw.println("Multicast: " + (mMulticastFilter ? "DROP" : "ALLOW")); |
| pw.println("Minimum RDNSS lifetime: " + mMinRdnssLifetimeSec); |
| try { |
| pw.println("IPv4 address: " + InetAddress.getByAddress(mIPv4Address).getHostAddress()); |
| } catch (UnknownHostException|NullPointerException e) {} |
| |
| if (mLastTimeInstalledProgram == 0) { |
| pw.println("No program installed."); |
| return; |
| } |
| pw.println("Program updates: " + mNumProgramUpdates); |
| pw.println(String.format( |
| "Last program length %d, installed %ds ago, lifetime %ds", |
| mLastInstalledProgram.length, secondsSinceBoot() - mLastTimeInstalledProgram, |
| mLastInstalledProgramMinLifetime)); |
| |
| pw.print("Denylisted Ethertypes:"); |
| for (int p : mEthTypeBlackList) { |
| pw.print(String.format(" %04x", p)); |
| } |
| pw.println(); |
| pw.println("RA filters:"); |
| pw.increaseIndent(); |
| for (Ra ra: mRas) { |
| pw.println(ra); |
| pw.increaseIndent(); |
| pw.println(String.format( |
| "Last seen %ds ago", secondsSinceBoot() - ra.mLastSeen)); |
| if (DBG) { |
| pw.println("Last match:"); |
| pw.increaseIndent(); |
| pw.println(ra.getLastMatchingPacket()); |
| pw.decreaseIndent(); |
| } |
| pw.decreaseIndent(); |
| } |
| pw.decreaseIndent(); |
| |
| pw.println("TCP Keepalive filters:"); |
| pw.increaseIndent(); |
| for (int i = 0; i < mKeepalivePackets.size(); ++i) { |
| final KeepalivePacket keepalivePacket = mKeepalivePackets.valueAt(i); |
| if (keepalivePacket instanceof TcpKeepaliveAck) { |
| pw.print("Slot "); |
| pw.print(mKeepalivePackets.keyAt(i)); |
| pw.print(": "); |
| pw.println(keepalivePacket); |
| } |
| } |
| pw.decreaseIndent(); |
| |
| pw.println("NAT-T Keepalive filters:"); |
| pw.increaseIndent(); |
| for (int i = 0; i < mKeepalivePackets.size(); ++i) { |
| final KeepalivePacket keepalivePacket = mKeepalivePackets.valueAt(i); |
| if (keepalivePacket instanceof NattKeepaliveResponse) { |
| pw.print("Slot "); |
| pw.print(mKeepalivePackets.keyAt(i)); |
| pw.print(": "); |
| pw.println(keepalivePacket); |
| } |
| } |
| pw.decreaseIndent(); |
| |
| if (DBG) { |
| pw.println("Last program:"); |
| pw.increaseIndent(); |
| pw.println(HexDump.toHexString(mLastInstalledProgram, false /* lowercase */)); |
| pw.decreaseIndent(); |
| } |
| |
| pw.println("APF packet counters: "); |
| pw.increaseIndent(); |
| if (!mApfCapabilities.hasDataAccess()) { |
| pw.println("APF counters not supported"); |
| } else if (mDataSnapshot == null) { |
| pw.println("No last snapshot."); |
| } else { |
| try { |
| Counter[] counters = Counter.class.getEnumConstants(); |
| for (Counter c : Arrays.asList(counters).subList(1, counters.length)) { |
| long value = ApfCounterTracker.getCounterValue(mDataSnapshot, c); |
| // Only print non-zero counters |
| if (value != 0) { |
| pw.println(c.toString() + ": " + value); |
| } |
| |
| // If the counter's value decreases, it may have been cleaned up or there may be |
| // a bug. |
| if (value < mApfCounterTracker.getCounters().getOrDefault(c, 0L)) { |
| Log.e(TAG, "Error: Counter value unexpectedly decreased."); |
| } |
| } |
| } catch (ArrayIndexOutOfBoundsException e) { |
| pw.println("Uh-oh: " + e); |
| } |
| if (VDBG) { |
| pw.println("Raw data dump: "); |
| pw.println(HexDump.dumpHexString(mDataSnapshot)); |
| } |
| } |
| pw.decreaseIndent(); |
| } |
| |
| /** Return ApfFilter update status for testing purposes. */ |
| public boolean isRunning() { |
| return mIsRunning; |
| } |
| |
| /** Pause ApfFilter updates for testing purposes. */ |
| public void pause() { |
| mIsRunning = false; |
| } |
| |
| /** Resume ApfFilter updates for testing purposes. */ |
| public void resume() { |
| mIsRunning = true; |
| } |
| |
| /** Return data snapshot as hex string for testing purposes. */ |
| public synchronized @Nullable String getDataSnapshotHexString() { |
| if (mDataSnapshot == null) { |
| return null; |
| } |
| return HexDump.toHexString(mDataSnapshot, 0, mDataSnapshot.length, false /* lowercase */); |
| } |
| |
| // TODO: move to android.net.NetworkUtils |
| @VisibleForTesting |
| public static int ipv4BroadcastAddress(byte[] addrBytes, int prefixLength) { |
| return bytesToBEInt(addrBytes) | (int) (Integer.toUnsignedLong(-1) >>> prefixLength); |
| } |
| |
| private static int uint8(byte b) { |
| return b & 0xff; |
| } |
| |
| private static int getUint16(ByteBuffer buffer, int position) { |
| return buffer.getShort(position) & 0xffff; |
| } |
| |
| private static long getUint32(ByteBuffer buffer, int position) { |
| return Integer.toUnsignedLong(buffer.getInt(position)); |
| } |
| |
| private static int getUint8(ByteBuffer buffer, int position) { |
| return uint8(buffer.get(position)); |
| } |
| |
| private static int bytesToBEInt(byte[] bytes) { |
| return (uint8(bytes[0]) << 24) |
| + (uint8(bytes[1]) << 16) |
| + (uint8(bytes[2]) << 8) |
| + (uint8(bytes[3])); |
| } |
| |
| private static byte[] concatArrays(final byte[]... arr) { |
| int size = 0; |
| for (byte[] a : arr) { |
| size += a.length; |
| } |
| final byte[] result = new byte[size]; |
| int offset = 0; |
| for (byte[] a : arr) { |
| System.arraycopy(a, 0, result, offset, a.length); |
| offset += a.length; |
| } |
| return result; |
| } |
| |
| private void sendNetworkQuirkMetrics(final NetworkQuirkEvent event) { |
| if (mNetworkQuirkMetrics == null) return; |
| mNetworkQuirkMetrics.setEvent(event); |
| mNetworkQuirkMetrics.statsWrite(); |
| } |
| } |