From 5e15d7b06ac59ef599a690dd7daf40ebb5fa0f12 Mon Sep 17 00:00:00 2001 From: Lorenzo Colitti Date: Fri, 14 Feb 2020 01:06:35 +0900 Subject: [PATCH] Tethering offload: add/remove IPv6 forwarding rules on ND events. Use IpNeighborMonitor to listen for ND cache events on the downstream interface, and push downstream IPv6 fowarding rules to netd. Rules are pushed when: - IPv6 neighbours appear/disappear on the downstream interface. - The upstream changes. Test: new unit test Change-Id: I7b01ba179a4d6bb248fd6c4994e48800613a4efa --- Tethering/src/android/net/ip/IpServer.java | 143 +++++++++++++++++- .../unit/src/android/net/ip/IpServerTest.java | 111 ++++++++++++++ .../connectivity/tethering/TetheringTest.java | 7 + 3 files changed, 260 insertions(+), 1 deletion(-) diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java index b4d49c02b0..0d369dcff8 100644 --- a/Tethering/src/android/net/ip/IpServer.java +++ b/Tethering/src/android/net/ip/IpServer.java @@ -40,12 +40,14 @@ import android.net.dhcp.DhcpServingParamsParcel; import android.net.dhcp.DhcpServingParamsParcelExt; import android.net.dhcp.IDhcpLeaseCallbacks; import android.net.dhcp.IDhcpServer; +import android.net.ip.IpNeighborMonitor.NeighborEvent; import android.net.ip.RouterAdvertisementDaemon.RaParams; import android.net.shared.NetdUtils; import android.net.shared.RouteUtils; import android.net.util.InterfaceParams; import android.net.util.InterfaceSet; import android.net.util.SharedLog; +import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.RemoteException; @@ -59,14 +61,17 @@ import com.android.internal.util.MessageUtils; import com.android.internal.util.State; import com.android.internal.util.StateMachine; +import java.io.IOException; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; +import java.net.NetworkInterface; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Objects; import java.util.Random; @@ -149,6 +154,12 @@ public class IpServer extends StateMachine { /** Capture IpServer dependencies, for injection. */ public abstract static class Dependencies { + /** Create an IpNeighborMonitor to be used by this IpServer */ + public IpNeighborMonitor getIpNeighborMonitor(Handler handler, SharedLog log, + IpNeighborMonitor.NeighborEventConsumer consumer) { + return new IpNeighborMonitor(handler, log, consumer); + } + /** Create a RouterAdvertisementDaemon instance to be used by IpServer.*/ public RouterAdvertisementDaemon getRouterAdvertisementDaemon(InterfaceParams ifParams) { return new RouterAdvertisementDaemon(ifParams); @@ -159,6 +170,15 @@ public class IpServer extends StateMachine { return InterfaceParams.getByName(ifName); } + /** Get |ifName|'s interface index. */ + public int getIfindex(String ifName) { + try { + return NetworkInterface.getByName(ifName).getIndex(); + } catch (IOException | NullPointerException e) { + Log.e(TAG, "Can't determine interface index for interface " + ifName); + return 0; + } + } /** Create a DhcpServer instance to be used by IpServer. */ public abstract void makeDhcpServer(String ifName, DhcpServingParamsParcel params, DhcpServerCallbacks cb); @@ -184,6 +204,8 @@ public class IpServer extends StateMachine { public static final int CMD_TETHER_CONNECTION_CHANGED = BASE_IPSERVER + 9; // new IPv6 tethering parameters need to be processed public static final int CMD_IPV6_TETHER_UPDATE = BASE_IPSERVER + 10; + // new neighbor cache entry on our interface + public static final int CMD_NEIGHBOR_EVENT = BASE_IPSERVER + 11; private final State mInitialState; private final State mLocalHotspotState; @@ -223,6 +245,40 @@ public class IpServer extends StateMachine { @NonNull private List mDhcpLeases = Collections.emptyList(); + private int mLastIPv6UpstreamIfindex = 0; + + private class MyNeighborEventConsumer implements IpNeighborMonitor.NeighborEventConsumer { + public void accept(NeighborEvent e) { + sendMessage(CMD_NEIGHBOR_EVENT, e); + } + } + + static class Ipv6ForwardingRule { + public final int upstreamIfindex; + public final int downstreamIfindex; + public final Inet6Address address; + public final MacAddress srcMac; + public final MacAddress dstMac; + + Ipv6ForwardingRule(int upstreamIfindex, int downstreamIfIndex, Inet6Address address, + MacAddress srcMac, MacAddress dstMac) { + this.upstreamIfindex = upstreamIfindex; + this.downstreamIfindex = downstreamIfIndex; + this.address = address; + this.srcMac = srcMac; + this.dstMac = dstMac; + } + + public Ipv6ForwardingRule onNewUpstream(int newUpstreamIfindex) { + return new Ipv6ForwardingRule(newUpstreamIfindex, downstreamIfindex, address, srcMac, + dstMac); + } + } + private final LinkedHashMap mIpv6ForwardingRules = + new LinkedHashMap<>(); + + private final IpNeighborMonitor mIpNeighborMonitor; + public IpServer( String ifaceName, Looper looper, int interfaceType, SharedLog log, INetd netd, Callback callback, boolean usingLegacyDhcp, Dependencies deps) { @@ -240,6 +296,12 @@ public class IpServer extends StateMachine { mLastError = TetheringManager.TETHER_ERROR_NO_ERROR; mServingMode = STATE_AVAILABLE; + mIpNeighborMonitor = mDeps.getIpNeighborMonitor(getHandler(), mLog, + new MyNeighborEventConsumer()); + if (!mIpNeighborMonitor.start()) { + mLog.e("Failed to create IpNeighborMonitor on " + mIfaceName); + } + mInitialState = new InitialState(); mLocalHotspotState = new LocalHotspotState(); mTetheredState = new TetheredState(); @@ -607,13 +669,16 @@ public class IpServer extends StateMachine { } RaParams params = null; + int upstreamIfindex = 0; if (v6only != null) { + final String upstreamIface = v6only.getInterfaceName(); + params = new RaParams(); params.mtu = v6only.getMtu(); params.hasDefaultRoute = v6only.hasIpv6DefaultRoute(); - if (params.hasDefaultRoute) params.hopLimit = getHopLimit(v6only.getInterfaceName()); + if (params.hasDefaultRoute) params.hopLimit = getHopLimit(upstreamIface); for (LinkAddress linkAddr : v6only.getLinkAddresses()) { if (linkAddr.getPrefixLength() != RFC7421_PREFIX_LENGTH) continue; @@ -627,12 +692,18 @@ public class IpServer extends StateMachine { params.dnses.add(dnsServer); } } + + upstreamIfindex = mDeps.getIfindex(upstreamIface); } + // If v6only is null, we pass in null to setRaParams(), which handles // deprecation of any existing RA data. setRaParams(params); mLastIPv6LinkProperties = v6only; + + updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, upstreamIfindex, null); + mLastIPv6UpstreamIfindex = upstreamIfindex; } private void configureLocalIPv6Routes( @@ -727,6 +798,73 @@ public class IpServer extends StateMachine { } } + private void addIpv6ForwardingRule(Ipv6ForwardingRule rule) { + try { + mNetd.tetherRuleAddDownstreamIpv6(mInterfaceParams.index, rule.upstreamIfindex, + rule.address.getAddress(), mInterfaceParams.macAddr.toByteArray(), + rule.dstMac.toByteArray()); + mIpv6ForwardingRules.put(rule.address, rule); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Could not add IPv6 downstream rule: " + e); + } + } + + private void removeIpv6ForwardingRule(Ipv6ForwardingRule rule, boolean removeFromMap) { + try { + mNetd.tetherRuleRemoveDownstreamIpv6(rule.upstreamIfindex, rule.address.getAddress()); + if (removeFromMap) { + mIpv6ForwardingRules.remove(rule.address); + } + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Could not remove IPv6 downstream rule: " + e); + } + } + + // Convenience method to replace a rule with the same rule on a new upstream interface. + // Allows replacing the rules in one iteration pass without ConcurrentModificationExceptions. + // Relies on the fact that rules are in a map indexed by IP address. + private void updateIpv6ForwardingRule(Ipv6ForwardingRule rule, int newIfindex) { + addIpv6ForwardingRule(rule.onNewUpstream(newIfindex)); + removeIpv6ForwardingRule(rule, false /*removeFromMap*/); + } + + // Handles all updates to IPv6 forwarding rules. These can currently change only if the upstream + // changes or if a neighbor event is received. + private void updateIpv6ForwardingRules(int prevUpstreamIfindex, int upstreamIfindex, + NeighborEvent e) { + // If the upstream interface has changed, remove all rules and re-add them with the new + // upstream interface. + if (prevUpstreamIfindex != upstreamIfindex) { + for (Ipv6ForwardingRule rule : mIpv6ForwardingRules.values()) { + updateIpv6ForwardingRule(rule, upstreamIfindex); + } + } + + // If we're here to process a NeighborEvent, do so now. + if (e == null) return; + if (!(e.ip instanceof Inet6Address) || e.ip.isMulticastAddress() + || e.ip.isLoopbackAddress() || e.ip.isLinkLocalAddress()) { + return; + } + + Ipv6ForwardingRule rule = new Ipv6ForwardingRule(mLastIPv6UpstreamIfindex, + mInterfaceParams.index, (Inet6Address) e.ip, mInterfaceParams.macAddr, + e.macAddr); + if (e.isValid()) { + addIpv6ForwardingRule(rule); + } else { + removeIpv6ForwardingRule(rule, true /*removeFromMap*/); + } + } + + private void handleNeighborEvent(NeighborEvent e) { + if (mInterfaceParams != null + && mInterfaceParams.index == e.ifindex + && mInterfaceParams.hasMacAddress) { + updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, mLastIPv6UpstreamIfindex, e); + } + } + private byte getHopLimit(String upstreamIface) { try { int upstreamHopLimit = Integer.parseUnsignedInt( @@ -1014,6 +1152,9 @@ public class IpServer extends StateMachine { } } break; + case CMD_NEIGHBOR_EVENT: + handleNeighborEvent((NeighborEvent) message.obj); + break; default: return false; } diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java index 6504195597..33b35586ee 100644 --- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java +++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java @@ -29,6 +29,11 @@ import static android.net.ip.IpServer.STATE_AVAILABLE; import static android.net.ip.IpServer.STATE_LOCAL_ONLY; import static android.net.ip.IpServer.STATE_TETHERED; import static android.net.ip.IpServer.STATE_UNAVAILABLE; +import static android.net.netlink.NetlinkConstants.RTM_DELNEIGH; +import static android.net.netlink.NetlinkConstants.RTM_NEWNEIGH; +import static android.net.netlink.StructNdMsg.NUD_FAILED; +import static android.net.netlink.StructNdMsg.NUD_REACHABLE; +import static android.net.netlink.StructNdMsg.NUD_STALE; import static android.net.shared.Inet4AddressUtils.intToInet4AddressHTH; import static org.junit.Assert.assertEquals; @@ -41,6 +46,7 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; @@ -52,6 +58,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.net.INetd; +import android.net.InetAddresses; import android.net.InterfaceConfigurationParcel; import android.net.IpPrefix; import android.net.LinkAddress; @@ -61,6 +68,8 @@ import android.net.RouteInfo; import android.net.dhcp.DhcpServingParamsParcel; import android.net.dhcp.IDhcpServer; import android.net.dhcp.IDhcpServerCallbacks; +import android.net.ip.IpNeighborMonitor.NeighborEvent; +import android.net.ip.IpNeighborMonitor.NeighborEventConsumer; import android.net.util.InterfaceParams; import android.net.util.InterfaceSet; import android.net.util.SharedLog; @@ -81,6 +90,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.net.Inet4Address; +import java.net.InetAddress; @RunWith(AndroidJUnit4.class) @SmallTest @@ -88,6 +98,8 @@ public class IpServerTest { private static final String IFACE_NAME = "testnet1"; private static final String UPSTREAM_IFACE = "upstream0"; private static final String UPSTREAM_IFACE2 = "upstream1"; + private static final int UPSTREAM_IFINDEX = 101; + private static final int UPSTREAM_IFINDEX2 = 102; private static final String BLUETOOTH_IFACE_ADDR = "192.168.42.1"; private static final int BLUETOOTH_DHCP_PREFIX_LENGTH = 24; private static final int DHCP_LEASE_TIME_SECS = 3600; @@ -102,6 +114,7 @@ public class IpServerTest { @Mock private SharedLog mSharedLog; @Mock private IDhcpServer mDhcpServer; @Mock private RouterAdvertisementDaemon mRaDaemon; + @Mock private IpNeighborMonitor mIpNeighborMonitor; @Mock private IpServer.Dependencies mDependencies; @Captor private ArgumentCaptor mDhcpParamsCaptor; @@ -111,6 +124,7 @@ public class IpServerTest { ArgumentCaptor.forClass(LinkProperties.class); private IpServer mIpServer; private InterfaceConfigurationParcel mInterfaceConfiguration; + private NeighborEventConsumer mNeighborEventConsumer; private void initStateMachine(int interfaceType) throws Exception { initStateMachine(interfaceType, false /* usingLegacyDhcp */); @@ -130,16 +144,28 @@ public class IpServerTest { }).when(mDependencies).makeDhcpServer(any(), mDhcpParamsCaptor.capture(), any()); when(mDependencies.getRouterAdvertisementDaemon(any())).thenReturn(mRaDaemon); when(mDependencies.getInterfaceParams(IFACE_NAME)).thenReturn(TEST_IFACE_PARAMS); + + when(mDependencies.getIfindex(eq(UPSTREAM_IFACE))).thenReturn(UPSTREAM_IFINDEX); + when(mDependencies.getIfindex(eq(UPSTREAM_IFACE2))).thenReturn(UPSTREAM_IFINDEX2); + mInterfaceConfiguration = new InterfaceConfigurationParcel(); mInterfaceConfiguration.flags = new String[0]; if (interfaceType == TETHERING_BLUETOOTH) { mInterfaceConfiguration.ipv4Addr = BLUETOOTH_IFACE_ADDR; mInterfaceConfiguration.prefixLength = BLUETOOTH_DHCP_PREFIX_LENGTH; } + + ArgumentCaptor neighborCaptor = + ArgumentCaptor.forClass(NeighborEventConsumer.class); + doReturn(mIpNeighborMonitor).when(mDependencies).getIpNeighborMonitor(any(), any(), + neighborCaptor.capture()); + mIpServer = new IpServer( IFACE_NAME, mLooper.getLooper(), interfaceType, mSharedLog, mNetd, mCallback, usingLegacyDhcp, mDependencies); mIpServer.start(); + mNeighborEventConsumer = neighborCaptor.getValue(); + // Starting the state machine always puts us in a consistent state and notifies // the rest of the world that we've changed from an unknown to available state. mLooper.dispatchAll(); @@ -172,6 +198,8 @@ public class IpServerTest { @Test public void startsOutAvailable() { + when(mDependencies.getIpNeighborMonitor(any(), any(), any())) + .thenReturn(mIpNeighborMonitor); mIpServer = new IpServer(IFACE_NAME, mLooper.getLooper(), TETHERING_BLUETOOTH, mSharedLog, mNetd, mCallback, false /* usingLegacyDhcp */, mDependencies); mIpServer.start(); @@ -469,6 +497,89 @@ public class IpServerTest { verify(mDependencies, never()).makeDhcpServer(any(), any(), any()); } + private InetAddress addr(String addr) throws Exception { + return InetAddresses.parseNumericAddress(addr); + } + + private void recvNewNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) { + mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_NEWNEIGH, ifindex, addr, + nudState, mac)); + mLooper.dispatchAll(); + } + + private void recvDelNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) { + mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_DELNEIGH, ifindex, addr, + nudState, mac)); + mLooper.dispatchAll(); + } + + @Test + public void addRemoveipv6ForwardingRules() throws Exception { + initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */); + + final int myIfindex = TEST_IFACE_PARAMS.index; + final int notMyIfindex = myIfindex - 1; + + final MacAddress myMac = TEST_IFACE_PARAMS.macAddr; + final InetAddress neighA = InetAddresses.parseNumericAddress("2001:db8::1"); + final InetAddress neighB = InetAddresses.parseNumericAddress("2001:db8::2"); + final InetAddress neighLL = InetAddresses.parseNumericAddress("fe80::1"); + final InetAddress neighMC = InetAddresses.parseNumericAddress("ff02::1234"); + final MacAddress macA = MacAddress.fromString("00:00:00:00:00:0a"); + final MacAddress macB = MacAddress.fromString("11:22:33:00:00:0b"); + + reset(mNetd); + + // Events on other interfaces are ignored. + recvNewNeigh(notMyIfindex, neighA, NUD_REACHABLE, macA); + verifyNoMoreInteractions(mNetd); + + // Events on this interface are received and sent to netd. + recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA); + verify(mNetd).tetherRuleAddDownstreamIpv6(eq(myIfindex), eq(UPSTREAM_IFINDEX), + eq(neighA.getAddress()), eq(myMac.toByteArray()), eq(macA.toByteArray())); + reset(mNetd); + + recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB); + verify(mNetd).tetherRuleAddDownstreamIpv6(eq(myIfindex), eq(UPSTREAM_IFINDEX), + eq(neighB.getAddress()), eq(myMac.toByteArray()), eq(macB.toByteArray())); + reset(mNetd); + + // Link-local and multicast neighbors are ignored. + recvNewNeigh(notMyIfindex, neighLL, NUD_REACHABLE, macA); + verifyNoMoreInteractions(mNetd); + recvNewNeigh(notMyIfindex, neighMC, NUD_REACHABLE, macA); + verifyNoMoreInteractions(mNetd); + + // A neighbor that is no longer valid causes the rule to be removed. + recvNewNeigh(myIfindex, neighA, NUD_FAILED, macA); + verify(mNetd).tetherRuleRemoveDownstreamIpv6(eq(UPSTREAM_IFINDEX), eq(neighA.getAddress())); + reset(mNetd); + + // A neighbor that is deleted causes the rule to be removed. + recvDelNeigh(myIfindex, neighB, NUD_STALE, macB); + verify(mNetd).tetherRuleRemoveDownstreamIpv6(eq(UPSTREAM_IFINDEX), eq(neighB.getAddress())); + reset(mNetd); + + // Upstream changes result in deleting and re-adding the rules. + recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA); + recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB); + reset(mNetd); + + InOrder inOrder = inOrder(mNetd); + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(UPSTREAM_IFACE2); + dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp); + inOrder.verify(mNetd).tetherRuleAddDownstreamIpv6(eq(myIfindex), eq(UPSTREAM_IFINDEX2), + eq(neighA.getAddress()), eq(myMac.toByteArray()), eq(macA.toByteArray())); + inOrder.verify(mNetd).tetherRuleRemoveDownstreamIpv6(eq(UPSTREAM_IFINDEX), + eq(neighA.getAddress())); + inOrder.verify(mNetd).tetherRuleAddDownstreamIpv6(eq(myIfindex), eq(UPSTREAM_IFINDEX2), + eq(neighB.getAddress()), eq(myMac.toByteArray()), eq(macB.toByteArray())); + inOrder.verify(mNetd).tetherRuleRemoveDownstreamIpv6(eq(UPSTREAM_IFINDEX), + eq(neighB.getAddress())); + } + private void assertDhcpStarted(IpPrefix expectedPrefix) throws Exception { verify(mDependencies, times(1)).makeDhcpServer(eq(IFACE_NAME), any(), any()); verify(mDhcpServer, timeout(MAKE_DHCPSERVER_TIMEOUT_MS).times(1)).startWithCallbacks( diff --git a/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java index 150b76ed2a..d14c62a981 100644 --- a/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java +++ b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java @@ -95,6 +95,7 @@ import android.net.TetheringRequestParcel; import android.net.dhcp.DhcpServerCallbacks; import android.net.dhcp.DhcpServingParamsParcel; import android.net.dhcp.IDhcpServer; +import android.net.ip.IpNeighborMonitor; import android.net.ip.IpServer; import android.net.ip.RouterAdvertisementDaemon; import android.net.util.InterfaceParams; @@ -173,6 +174,7 @@ public class TetheringTest { @Mock private UpstreamNetworkMonitor mUpstreamNetworkMonitor; @Mock private IPv6TetheringCoordinator mIPv6TetheringCoordinator; @Mock private RouterAdvertisementDaemon mRouterAdvertisementDaemon; + @Mock private IpNeighborMonitor mIpNeighborMonitor; @Mock private IDhcpServer mDhcpServer; @Mock private INetd mNetd; @Mock private UserManager mUserManager; @@ -278,6 +280,11 @@ public class TetheringTest { } }).run(); } + + public IpNeighborMonitor getIpNeighborMonitor(Handler h, SharedLog l, + IpNeighborMonitor.NeighborEventConsumer c) { + return mIpNeighborMonitor; + } } private class MockTetheringConfiguration extends TetheringConfiguration {