blob: 25c84e1328046a92420911bc7d7051f56d7509da [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.connectivity;
import static android.system.OsConstants.*;
import android.net.NetworkUtils;
import android.net.apf.ApfGenerator;
import android.net.apf.ApfGenerator.IllegalInstructionException;
import android.net.apf.ApfGenerator.Register;
import android.system.ErrnoException;
import android.system.Os;
import android.system.PacketSocketAddress;
import android.util.Log;
import android.util.Pair;
import com.android.internal.util.HexDump;
import com.android.server.ConnectivityService;
import java.io.FileDescriptor;
import java.io.IOException;
import java.lang.Thread;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import libcore.io.IoBridge;
/**
* 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.
*
* @hide
*/
public class ApfFilter {
// Thread to listen for RAs.
private 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;
try {
// Interrupts the read() call the thread is blocked in.
IoBridge.closeAndSignalBlockedThreads(mSocket);
} catch (IOException ignored) {}
}
@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 final ConnectivityService mConnectivityService;
private final NetworkAgentInfo mNai;
private ReceiveThread mReceiveThread;
private String mIfaceName;
private long mUniqueCounter;
private ApfFilter(ConnectivityService connectivityService, NetworkAgentInfo nai) {
mConnectivityService = connectivityService;
mNai = nai;
maybeStartFilter();
}
private void log(String s) {
Log.d(TAG, "(" + mNai.network.netId + "): " + s);
}
private long getUniqueNumber() {
return mUniqueCounter++;
}
/**
* Attempt to start listening for RAs and, if RAs are received, generating and installing
* filters to ignore useless RAs.
*/
private void maybeStartFilter() {
mIfaceName = mNai.linkProperties.getInterfaceName();
if (mIfaceName == null) return;
FileDescriptor socket;
try {
socket = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6);
PacketSocketAddress addr = new PacketSocketAddress((short) ETH_P_IPV6,
NetworkInterface.getByName(mIfaceName).getIndex());
Os.bind(socket, addr);
NetworkUtils.attachRaFilter(socket, mNai.networkMisc.apfPacketFormat);
} catch(SocketException|ErrnoException e) {
Log.e(TAG, "Error filtering raw socket", e);
return;
}
mReceiveThread = new ReceiveThread(socket);
mReceiveThread.start();
}
/**
* mNai's LinkProperties may have changed, take appropriate action.
*/
public void updateFilter() {
// If we're not listening for RAs, try starting.
if (mReceiveThread == null) {
maybeStartFilter();
// If interface name has changed, restart.
} else if (!mIfaceName.equals(mNai.linkProperties.getInterfaceName())) {
shutdown();
maybeStartFilter();
}
}
// Returns seconds since Unix Epoch.
private static long curTime() {
return System.currentTimeMillis() / 1000L;
}
// A class to hold information about an RA.
private class Ra {
private static final int ETH_HEADER_LEN = 14;
private static final int IPV6_HEADER_LEN = 40;
// From RFC4861:
private static final int ICMP6_RA_HEADER_LEN = 16;
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_LEN = 32;
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_OFFSET = 8;
private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN = 4;
// 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 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;
private final ByteBuffer mPacket;
// List of binary ranges that include the whole packet except the lifetimes.
// Pairs consist of offset and length.
private final ArrayList<Pair<Integer, Integer>> mNonLifetimes =
new ArrayList<Pair<Integer, Integer>>();
// Minimum lifetime in packet
long mMinLifetime;
// When the packet was last captured, in seconds since Unix Epoch
long mLastSeen;
/**
* Add a binary range of the packet that does not include a lifetime to mNonLifetimes.
* Assumes mPacket.position() is as far as we've parsed the packet.
* @param lastNonLifetimeStart offset within packet of where the last binary range of
* data not including a lifetime.
* @param lifetimeOffset offset from mPacket.position() to the next lifetime data.
* @param lifetimeLength length of the next lifetime data.
* @return offset within packet of where the next binary range of data not including
* a lifetime. This can be passed into the next invocation of this function
* via {@code lastNonLifetimeStart}.
*/
private int addNonLifetime(int lastNonLifetimeStart, int lifetimeOffset,
int lifetimeLength) {
lifetimeOffset += mPacket.position();
mNonLifetimes.add(new Pair<Integer, Integer>(lastNonLifetimeStart,
lifetimeOffset - lastNonLifetimeStart));
return lifetimeOffset + lifetimeLength;
}
// Note that this parses RA and may throw IllegalArgumentException (from
// Buffer.position(int) ) or IndexOutOfBoundsException (from ByteBuffer.get(int) ) if
// parsing encounters something non-compliant with specifications.
Ra(byte[] packet, int length) {
mPacket = ByteBuffer.allocate(length).put(ByteBuffer.wrap(packet, 0, length));
mPacket.clear();
mLastSeen = curTime();
// Parse router lifetime
int lastNonLifetimeStart = addNonLifetime(0, ICMP6_RA_ROUTER_LIFETIME_OFFSET,
ICMP6_RA_ROUTER_LIFETIME_LEN);
// Parse ICMP6 options
mPacket.position(ICMP6_RA_OPTION_OFFSET);
while (mPacket.hasRemaining()) {
int optionType = ((int)mPacket.get(mPacket.position())) & 0xff;
int optionLength = (((int)mPacket.get(mPacket.position() + 1)) & 0xff) * 8;
switch (optionType) {
case ICMP6_PREFIX_OPTION_TYPE:
// Parse valid lifetime
lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart,
ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET,
ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN);
// Parse preferred lifetime
lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart,
ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET,
ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN);
break;
// These three options have the same lifetime offset and size, so process
// together:
case ICMP6_ROUTE_INFO_OPTION_TYPE:
case ICMP6_RDNSS_OPTION_TYPE:
case ICMP6_DNSSL_OPTION_TYPE:
// Parse lifetime
lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart,
ICMP6_4_BYTE_LIFETIME_OFFSET,
ICMP6_4_BYTE_LIFETIME_LEN);
break;
default:
// RFC4861 section 4.2 dictates we ignore unknown options for fowards
// compatibility.
break;
}
mPacket.position(mPacket.position() + optionLength);
}
// Mark non-lifetime bytes since last lifetime.
addNonLifetime(lastNonLifetimeStart, 0, 0);
mMinLifetime = minLifetime(packet, length);
}
// Ignoring lifetimes (which may change) does {@code packet} match this RA?
boolean matches(byte[] packet, int length) {
if (length != mPacket.limit()) return false;
ByteBuffer a = ByteBuffer.wrap(packet);
ByteBuffer b = mPacket;
for (Pair<Integer, Integer> nonLifetime : mNonLifetimes) {
a.clear();
b.clear();
a.position(nonLifetime.first);
b.position(nonLifetime.first);
a.limit(nonLifetime.first + nonLifetime.second);
b.limit(nonLifetime.first + nonLifetime.second);
if (a.compareTo(b) != 0) return false;
}
return true;
}
// What is the minimum of all lifetimes within {@code packet} in seconds?
// Precondition: matches(packet, length) already returned true.
long minLifetime(byte[] packet, int length) {
long minLifetime = Long.MAX_VALUE;
// Wrap packet in ByteBuffer so we can read big-endian values easily
ByteBuffer byteBuffer = ByteBuffer.wrap(packet);
for (int i = 0; (i + 1) < mNonLifetimes.size(); i++) {
int offset = mNonLifetimes.get(i).first + mNonLifetimes.get(i).second;
int lifetimeLength = mNonLifetimes.get(i+1).first - offset;
long val;
switch (lifetimeLength) {
case 2: val = byteBuffer.getShort(offset); break;
case 4: val = byteBuffer.getInt(offset); break;
default: throw new IllegalStateException("bogus lifetime size " + length);
}
// Mask to size, converting signed to unsigned
val &= (1L << (lifetimeLength * 8)) - 1;
minLifetime = Math.min(minLifetime, val);
}
return minLifetime;
}
// How many seconds does this RA's have to live, taking into account the fact
// that we might have seen it a while ago.
long currentLifetime() {
return mMinLifetime - (curTime() - mLastSeen);
}
boolean isExpired() {
return currentLifetime() < 0;
}
// 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.
long generateFilter(ApfGenerator gen) throws IllegalInstructionException {
String nextFilterLabel = "Ra" + getUniqueNumber();
// Skip if packet is not the right size
gen.addLoadFromMemory(Register.R0, gen.PACKET_SIZE_MEMORY_SLOT);
gen.addJumpIfR0NotEquals(mPacket.limit(), nextFilterLabel);
int filterLifetime = (int)(currentLifetime() / FRACTION_OF_LIFETIME_TO_FILTER);
// Skip filter if expired
gen.addLoadFromMemory(Register.R0, gen.FILTER_AGE_MEMORY_SLOT);
gen.addJumpIfR0GreaterThan(filterLifetime, nextFilterLabel);
for (int i = 0; i < mNonLifetimes.size(); i++) {
// Generate code to match the packet bytes
Pair<Integer, Integer> nonLifetime = mNonLifetimes.get(i);
gen.addLoadImmediate(Register.R0, nonLifetime.first);
gen.addJumpIfBytesNotEqual(Register.R0,
Arrays.copyOfRange(mPacket.array(), nonLifetime.first,
nonLifetime.first + nonLifetime.second),
nextFilterLabel);
// Generate code to test the lifetimes haven't gone down too far
if ((i + 1) < mNonLifetimes.size()) {
Pair<Integer, Integer> nextNonLifetime = mNonLifetimes.get(i + 1);
int offset = nonLifetime.first + nonLifetime.second;
int length = nextNonLifetime.first - offset;
switch (length) {
case 4: gen.addLoad32(Register.R0, offset); break;
case 2: gen.addLoad16(Register.R0, offset); break;
default: throw new IllegalStateException("bogus lifetime size " + length);
}
gen.addJumpIfR0LessThan(filterLifetime, nextFilterLabel);
}
}
gen.addJump(gen.DROP_LABEL);
gen.defineLabel(nextFilterLabel);
return filterLifetime;
}
}
// Maximum number of RAs to filter for.
private static final int MAX_RAS = 10;
private ArrayList<Ra> mRas = new ArrayList<Ra>();
// There is always some marginal benefit to updating the installed APF program when an RA is
// seen because we can extend the program's lifetime slightly, but there is some cost to
// updating the program, so don't bother unless the program is going to expire soon. This
// constant defines "soon" in seconds.
private static final long MAX_PROGRAM_LIFETIME_WORTH_REFRESHING = 30;
// 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.
private long mLastTimeInstalledProgram;
// How long should the last installed filter program live for? In seconds.
private long mLastInstalledProgramMinLifetime;
private void installNewProgram() {
if (mRas.size() == 0) return;
final byte[] program;
long programMinLifetime = Long.MAX_VALUE;
try {
ApfGenerator gen = new ApfGenerator();
// This is guaranteed to return true because of the check in maybeInstall.
gen.setApfVersion(mNai.networkMisc.apfVersionSupported);
// Step 1: Determine how many RA filters we can fit in the program.
int ras = 0;
for (Ra ra : mRas) {
if (ra.isExpired()) continue;
ra.generateFilter(gen);
if (gen.programLengthOverEstimate() > mNai.networkMisc.maximumApfProgramSize) {
// We went too far. Use prior number of RAs in "ras".
break;
} else {
// Yay! this RA filter fits, increment "ras".
ras++;
}
}
// Step 2: Generate RA filters
gen = new ApfGenerator();
// This is guaranteed to return true because of the check in maybeInstall.
gen.setApfVersion(mNai.networkMisc.apfVersionSupported);
for (Ra ra : mRas) {
if (ras-- == 0) break;
if (ra.isExpired()) continue;
programMinLifetime = Math.min(programMinLifetime, ra.generateFilter(gen));
}
// Execution will reach the end of the program if no filters match, which will pass the
// packet to the AP.
program = gen.generate();
} catch (IllegalInstructionException e) {
Log.e(TAG, "Program failed to generate: ", e);
return;
}
mLastTimeInstalledProgram = curTime();
mLastInstalledProgramMinLifetime = programMinLifetime;
hexDump("Installing filter: ", program, program.length);
mConnectivityService.pushApfProgramToNetwork(mNai, program);
}
// Install a new filter program if the last installed one will die soon.
private void maybeInstallNewProgram() {
if (mRas.size() == 0) return;
// If the current program doesn't expire for a while, don't bother updating.
long expiry = mLastTimeInstalledProgram + mLastInstalledProgramMinLifetime;
if (expiry < curTime() + MAX_PROGRAM_LIFETIME_WORTH_REFRESHING) {
installNewProgram();
}
}
private void hexDump(String msg, byte[] packet, int length) {
log(msg + HexDump.toHexString(packet, 0, length));
}
private void processRa(byte[] packet, int length) {
hexDump("Read packet = ", packet, length);
// Have we seen this RA before?
for (int i = 0; i < mRas.size(); i++) {
Ra ra = mRas.get(i);
if (ra.matches(packet, length)) {
log("matched RA");
// Update lifetimes.
ra.mLastSeen = curTime();
ra.mMinLifetime = ra.minLifetime(packet, length);
// 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.add(0, mRas.remove(i));
maybeInstallNewProgram();
return;
}
}
// Purge expired RAs.
for (int i = 0; i < mRas.size();) {
if (mRas.get(i).isExpired()) {
log("expired RA");
mRas.remove(i);
} else {
i++;
}
}
// TODO: figure out how to proceed when we've received more then MAX_RAS RAs.
if (mRas.size() >= MAX_RAS) return;
try {
log("adding RA");
mRas.add(new Ra(packet, length));
} catch (Exception e) {
Log.e(TAG, "Error parsing RA: " + e);
return;
}
installNewProgram();
}
/**
* Install an {@link ApfFilter} on {@code nai} if {@code nai} supports packet
* filtering using APF programs.
*/
public static void maybeInstall(ConnectivityService connectivityService, NetworkAgentInfo nai) {
if (nai.networkMisc == null) return;
if (nai.networkMisc.apfVersionSupported == 0) return;
if (nai.networkMisc.maximumApfProgramSize < 200) {
Log.e(TAG, "Uselessly small APF size limit: " + nai.networkMisc.maximumApfProgramSize);
return;
}
// For now only support generating programs for Ethernet frames. If this restriction is
// lifted:
// 1. the program generator will need its offsets adjusted.
// 2. the packet filter attached to our packet socket will need its offset adjusted.
if (nai.networkMisc.apfPacketFormat != ARPHRD_ETHER) return;
if (!new ApfGenerator().setApfVersion(nai.networkMisc.apfVersionSupported)) {
Log.e(TAG, "Unsupported APF version: " + nai.networkMisc.apfVersionSupported);
return;
}
nai.apfFilter = new ApfFilter(connectivityService, nai);
}
public void shutdown() {
if (mReceiveThread != null) {
log("shuting down");
mReceiveThread.halt(); // Also closes socket.
mReceiveThread = null;
}
}
}