diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java index 8116057a33..3acc766577 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,8 +669,11 @@ public class IpServer extends StateMachine { } RaParams params = null; + int upstreamIfindex = 0; if (v6only != null) { + final String upstreamIface = v6only.getInterfaceName(); + params = new RaParams(); // We advertise an mtu lower by 16, which is the closest multiple of 8 >= 14, // the ethernet header size. This makes kernel ebpf tethering offload happy. @@ -618,7 +683,7 @@ public class IpServer extends StateMachine { params.mtu = v6only.getMtu() - 16; 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; @@ -632,12 +697,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( @@ -732,6 +803,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( @@ -1019,6 +1157,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 {