diff --git a/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java b/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java index 4f8ad8a221..4710a30b29 100644 --- a/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java +++ b/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java @@ -173,6 +173,18 @@ public class DhcpServingParamsParcelExt extends DhcpServingParamsParcel { return this; } + /** + * Set whether the DHCP server should request a new prefix from IpServer when receiving + * DHCPDECLINE message in certain particular link (e.g. there is only one downstream USB + * tethering client). If it's false, process DHCPDECLINE message as RFC2131#4.3.3 suggests. + * + *

If not set, the default value is false. + */ + public DhcpServingParamsParcelExt setChangePrefixOnDecline(boolean changePrefixOnDecline) { + this.changePrefixOnDecline = changePrefixOnDecline; + return this; + } + private static int[] toIntArray(@NonNull Collection addrs) { int[] res = new int[addrs.size()]; int i = 0; diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java index d993306b17..de537871a7 100644 --- a/Tethering/src/android/net/ip/IpServer.java +++ b/Tethering/src/android/net/ip/IpServer.java @@ -50,6 +50,7 @@ import android.net.shared.NetdUtils; import android.net.shared.RouteUtils; import android.net.util.InterfaceParams; import android.net.util.InterfaceSet; +import android.net.util.PrefixUtils; import android.net.util.SharedLog; import android.os.Handler; import android.os.Looper; @@ -60,6 +61,7 @@ import android.util.Log; import android.util.SparseArray; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.internal.util.MessageUtils; import com.android.internal.util.State; @@ -115,6 +117,15 @@ public class IpServer extends StateMachine { private static final String ETHERNET_IFACE_ADDR = "192.168.50.1"; private static final int ETHERNET_IFACE_PREFIX_LENGTH = 24; + // TODO: remove this constant after introducing PrivateAddressCoordinator. + private static final List NCM_PREFIXES = Collections.unmodifiableList( + Arrays.asList( + new IpPrefix("192.168.42.0/24"), + new IpPrefix("192.168.51.0/24"), + new IpPrefix("192.168.52.0/24"), + new IpPrefix("192.168.53.0/24") + )); + // TODO: have PanService use some visible version of this constant private static final String BLUETOOTH_IFACE_ADDR = "192.168.44.1"; private static final int BLUETOOTH_DHCP_PREFIX_LENGTH = 24; @@ -212,6 +223,8 @@ public class IpServer extends StateMachine { 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; + // request from DHCP server that it wants to have a new prefix + public static final int CMD_NEW_PREFIX_REQUEST = BASE_IPSERVER + 12; private final State mInitialState; private final State mLocalHotspotState; @@ -462,7 +475,7 @@ public class IpServer extends StateMachine { handleError(); } } - }, new DhcpLeaseCallback()); + }, new DhcpEventCallback()); } catch (RemoteException e) { throw new IllegalStateException(e); } @@ -475,7 +488,7 @@ public class IpServer extends StateMachine { } } - private class DhcpLeaseCallback extends IDhcpEventCallbacks.Stub { + private class DhcpEventCallback extends IDhcpEventCallbacks.Stub { @Override public void onLeasesChanged(List leaseParcelables) { final ArrayList leases = new ArrayList<>(); @@ -509,8 +522,9 @@ public class IpServer extends StateMachine { } @Override - public void onNewPrefixRequest(IpPrefix currentPrefix) { - //TODO: add specific implementation. + public void onNewPrefixRequest(@NonNull final IpPrefix currentPrefix) { + Objects.requireNonNull(currentPrefix); + sendMessage(CMD_NEW_PREFIX_REQUEST, currentPrefix); } @Override @@ -524,26 +538,38 @@ public class IpServer extends StateMachine { } } + private RouteInfo getDirectConnectedRoute(@NonNull final LinkAddress ipv4Address) { + Objects.requireNonNull(ipv4Address); + return new RouteInfo(PrefixUtils.asIpPrefix(ipv4Address), null, mIfaceName, RTN_UNICAST); + } + + private DhcpServingParamsParcel makeServingParams(@NonNull final Inet4Address defaultRouter, + @NonNull final Inet4Address dnsServer, @NonNull LinkAddress serverAddr, + @Nullable Inet4Address clientAddr) { + final boolean changePrefixOnDecline = + (mInterfaceType == TetheringManager.TETHERING_NCM && clientAddr == null); + return new DhcpServingParamsParcelExt() + .setDefaultRouters(defaultRouter) + .setDhcpLeaseTimeSecs(DHCP_LEASE_TIME_SECS) + .setDnsServers(dnsServer) + .setServerAddr(serverAddr) + .setMetered(true) + .setSingleClientAddr(clientAddr) + .setChangePrefixOnDecline(changePrefixOnDecline); + // TODO: also advertise link MTU + } + private boolean startDhcp(final LinkAddress serverLinkAddr, final LinkAddress clientLinkAddr) { if (mUsingLegacyDhcp) { return true; } final Inet4Address addr = (Inet4Address) serverLinkAddr.getAddress(); - final int prefixLen = serverLinkAddr.getPrefixLength(); final Inet4Address clientAddr = clientLinkAddr == null ? null : (Inet4Address) clientLinkAddr.getAddress(); - final DhcpServingParamsParcel params; - params = new DhcpServingParamsParcelExt() - .setDefaultRouters(addr) - .setDhcpLeaseTimeSecs(DHCP_LEASE_TIME_SECS) - .setDnsServers(addr) - .setServerAddr(serverLinkAddr) - .setMetered(true) - .setSingleClientAddr(clientAddr); - // TODO: also advertise link MTU - + final DhcpServingParamsParcel params = makeServingParams(addr /* defaultRouter */, + addr /* dnsServer */, serverLinkAddr, clientAddr); mDhcpServerStartIndex++; mDeps.makeDhcpServer( mIfaceName, params, new DhcpServerCallbacksImpl(mDhcpServerStartIndex)); @@ -570,7 +596,7 @@ public class IpServer extends StateMachine { }); mDhcpServer = null; } catch (RemoteException e) { - mLog.e("Error stopping DHCP", e); + mLog.e("Error stopping DHCP server", e); // Not much more we can do here } } @@ -652,31 +678,33 @@ public class IpServer extends StateMachine { return false; } - // Directly-connected route. - final IpPrefix ipv4Prefix = new IpPrefix(mIpv4Address.getAddress(), - mIpv4Address.getPrefixLength()); - final RouteInfo route = new RouteInfo(ipv4Prefix, null, null, RTN_UNICAST); if (enabled) { mLinkProperties.addLinkAddress(mIpv4Address); - mLinkProperties.addRoute(route); + mLinkProperties.addRoute(getDirectConnectedRoute(mIpv4Address)); } else { mLinkProperties.removeLinkAddress(mIpv4Address); - mLinkProperties.removeRoute(route); + mLinkProperties.removeRoute(getDirectConnectedRoute(mIpv4Address)); } - return configureDhcp(enabled, mIpv4Address, mStaticIpv4ClientAddr); } - private String getRandomWifiIPv4Address() { + private Inet4Address getRandomIPv4Address(@NonNull final byte[] rawAddr) { + final byte[] ipv4Addr = rawAddr; + ipv4Addr[3] = getRandomSanitizedByte(DOUG_ADAMS, asByte(0), asByte(1), FF); try { - byte[] bytes = parseNumericAddress(WIFI_HOST_IFACE_ADDR).getAddress(); - bytes[3] = getRandomSanitizedByte(DOUG_ADAMS, asByte(0), asByte(1), FF); - return InetAddress.getByAddress(bytes).getHostAddress(); - } catch (Exception e) { - return WIFI_HOST_IFACE_ADDR; + return (Inet4Address) InetAddress.getByAddress(ipv4Addr); + } catch (UnknownHostException e) { + mLog.e("Failed to construct Inet4Address from raw IPv4 addr"); + return null; } } + private String getRandomWifiIPv4Address() { + final Inet4Address ipv4Addr = + getRandomIPv4Address(parseNumericAddress(WIFI_HOST_IFACE_ADDR).getAddress()); + return ipv4Addr != null ? ipv4Addr.getHostAddress() : WIFI_HOST_IFACE_ADDR; + } + private boolean startIPv6() { mInterfaceParams = mDeps.getInterfaceParams(mIfaceName); if (mInterfaceParams == null) { @@ -761,21 +789,43 @@ public class IpServer extends StateMachine { mLastIPv6UpstreamIfindex = upstreamIfindex; } + private void removeRoutesFromLocalNetwork(@NonNull final List toBeRemoved) { + final int removalFailures = RouteUtils.removeRoutesFromLocalNetwork( + mNetd, toBeRemoved); + if (removalFailures > 0) { + mLog.e(String.format("Failed to remove %d IPv6 routes from local table.", + removalFailures)); + } + + for (RouteInfo route : toBeRemoved) mLinkProperties.removeRoute(route); + } + + private void addRoutesToLocalNetwork(@NonNull final List toBeAdded) { + try { + // It's safe to call networkAddInterface() even if + // the interface is already in the local_network. + mNetd.networkAddInterface(INetd.LOCAL_NET_ID, mIfaceName); + try { + // Add routes from local network. Note that adding routes that + // already exist does not cause an error (EEXIST is silently ignored). + RouteUtils.addRoutesToLocalNetwork(mNetd, mIfaceName, toBeAdded); + } catch (IllegalStateException e) { + mLog.e("Failed to add IPv4/v6 routes to local table: " + e); + return; + } + } catch (ServiceSpecificException | RemoteException e) { + mLog.e("Failed to add " + mIfaceName + " to local table: ", e); + return; + } + + for (RouteInfo route : toBeAdded) mLinkProperties.addRoute(route); + } + private void configureLocalIPv6Routes( HashSet deprecatedPrefixes, HashSet newPrefixes) { // [1] Remove the routes that are deprecated. if (!deprecatedPrefixes.isEmpty()) { - final ArrayList toBeRemoved = - getLocalRoutesFor(mIfaceName, deprecatedPrefixes); - // Remove routes from local network. - final int removalFailures = RouteUtils.removeRoutesFromLocalNetwork( - mNetd, toBeRemoved); - if (removalFailures > 0) { - mLog.e(String.format("Failed to remove %d IPv6 routes from local table.", - removalFailures)); - } - - for (RouteInfo route : toBeRemoved) mLinkProperties.removeRoute(route); + removeRoutesFromLocalNetwork(getLocalRoutesFor(mIfaceName, deprecatedPrefixes)); } // [2] Add only the routes that have not previously been added. @@ -786,24 +836,7 @@ public class IpServer extends StateMachine { } if (!addedPrefixes.isEmpty()) { - final ArrayList toBeAdded = - getLocalRoutesFor(mIfaceName, addedPrefixes); - try { - // It's safe to call networkAddInterface() even if - // the interface is already in the local_network. - mNetd.networkAddInterface(INetd.LOCAL_NET_ID, mIfaceName); - try { - // Add routes from local network. Note that adding routes that - // already exist does not cause an error (EEXIST is silently ignored). - RouteUtils.addRoutesToLocalNetwork(mNetd, mIfaceName, toBeAdded); - } catch (IllegalStateException e) { - mLog.e("Failed to add IPv6 routes to local table: " + e); - } - } catch (ServiceSpecificException | RemoteException e) { - mLog.e("Failed to add " + mIfaceName + " to local table: ", e); - } - - for (RouteInfo route : toBeAdded) mLinkProperties.addRoute(route); + addRoutesToLocalNetwork(getLocalRoutesFor(mIfaceName, addedPrefixes)); } } } @@ -945,6 +978,80 @@ public class IpServer extends StateMachine { } } + // TODO: call PrivateAddressCoordinator.requestDownstreamAddress instead of this temporary + // logic. + private Inet4Address requestDownstreamAddress(@NonNull final IpPrefix currentPrefix) { + final int oldIndex = NCM_PREFIXES.indexOf(currentPrefix); + if (oldIndex == -1) { + mLog.e("current prefix isn't supported for NCM link: " + currentPrefix); + return null; + } + + final IpPrefix newPrefix = NCM_PREFIXES.get((oldIndex + 1) % NCM_PREFIXES.size()); + return getRandomIPv4Address(newPrefix.getRawAddress()); + } + + private void handleNewPrefixRequest(@NonNull final IpPrefix currentPrefix) { + if (!currentPrefix.contains(mIpv4Address.getAddress()) + || currentPrefix.getPrefixLength() != mIpv4Address.getPrefixLength()) { + Log.e(TAG, "Invalid prefix: " + currentPrefix); + return; + } + + final LinkAddress deprecatedLinkAddress = mIpv4Address; + final Inet4Address srvAddr = requestDownstreamAddress(currentPrefix); + if (srvAddr == null) { + mLog.e("Fail to request a new downstream prefix"); + return; + } + mIpv4Address = new LinkAddress(srvAddr, currentPrefix.getPrefixLength()); + + // Add new IPv4 address on the interface. + if (!mInterfaceCtrl.addAddress(srvAddr, currentPrefix.getPrefixLength())) { + mLog.e("Failed to add new IP " + srvAddr); + return; + } + + // Remove deprecated routes from local network. + removeRoutesFromLocalNetwork( + Collections.singletonList(getDirectConnectedRoute(deprecatedLinkAddress))); + mLinkProperties.removeLinkAddress(deprecatedLinkAddress); + + // Add new routes to local network. + addRoutesToLocalNetwork( + Collections.singletonList(getDirectConnectedRoute(mIpv4Address))); + mLinkProperties.addLinkAddress(mIpv4Address); + + // Update local DNS caching server with new IPv4 address, otherwise, dnsmasq doesn't + // listen on the interface configured with new IPv4 address, that results DNS validation + // failure of downstream client even if appropriate routes have been configured. + try { + mNetd.tetherApplyDnsInterfaces(); + } catch (ServiceSpecificException | RemoteException e) { + mLog.e("Failed to update local DNS caching server"); + return; + } + sendLinkProperties(); + + // Notify DHCP server that new prefix/route has been applied on IpServer. + final Inet4Address clientAddr = mStaticIpv4ClientAddr == null ? null : + (Inet4Address) mStaticIpv4ClientAddr.getAddress(); + final DhcpServingParamsParcel params = makeServingParams(srvAddr /* defaultRouter */, + srvAddr /* dnsServer */, mIpv4Address /* serverLinkAddress */, clientAddr); + try { + mDhcpServer.updateParams(params, new OnHandlerStatusCallback() { + @Override + public void callback(int statusCode) { + if (statusCode != STATUS_SUCCESS) { + mLog.e("Error updating DHCP serving params: " + statusCode); + } + } + }); + } catch (RemoteException e) { + mLog.e("Error updating DHCP serving params", e); + } + } + private byte getHopLimit(String upstreamIface) { try { int upstreamHopLimit = Integer.parseUnsignedInt( @@ -1056,11 +1163,9 @@ public class IpServer extends StateMachine { } try { - final IpPrefix ipv4Prefix = new IpPrefix(mIpv4Address.getAddress(), - mIpv4Address.getPrefixLength()); - NetdUtils.tetherInterface(mNetd, mIfaceName, ipv4Prefix); + NetdUtils.tetherInterface(mNetd, mIfaceName, PrefixUtils.asIpPrefix(mIpv4Address)); } catch (RemoteException | ServiceSpecificException | IllegalStateException e) { - mLog.e("Error Tethering: " + e); + mLog.e("Error Tethering", e); mLastError = TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR; return; } @@ -1115,6 +1220,9 @@ public class IpServer extends StateMachine { mLastError = TetheringManager.TETHER_ERROR_INTERNAL_ERROR; transitionTo(mInitialState); break; + case CMD_NEW_PREFIX_REQUEST: + handleNewPrefixRequest((IpPrefix) 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 b9622da9d2..cd1ff607b4 100644 --- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java +++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java @@ -18,6 +18,7 @@ package android.net.ip; import static android.net.INetd.IF_STATE_UP; import static android.net.TetheringManager.TETHERING_BLUETOOTH; +import static android.net.TetheringManager.TETHERING_NCM; import static android.net.TetheringManager.TETHERING_USB; import static android.net.TetheringManager.TETHERING_WIFI; import static android.net.TetheringManager.TETHERING_WIFI_P2P; @@ -38,6 +39,7 @@ import static android.net.shared.Inet4AddressUtils.intToInet4AddressHTH; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -67,6 +69,7 @@ import android.net.MacAddress; import android.net.RouteInfo; import android.net.TetherOffloadRuleParcel; import android.net.dhcp.DhcpServingParamsParcel; +import android.net.dhcp.IDhcpEventCallbacks; import android.net.dhcp.IDhcpServer; import android.net.dhcp.IDhcpServerCallbacks; import android.net.ip.IpNeighborMonitor.NeighborEvent; @@ -94,6 +97,7 @@ import org.mockito.MockitoAnnotations; import java.net.Inet4Address; import java.net.InetAddress; import java.util.Arrays; +import java.util.List; @RunWith(AndroidJUnit4.class) @SmallTest @@ -496,6 +500,59 @@ public class IpServerTest { assertDhcpStarted(new IpPrefix("192.168.49.0/24")); } + @Test + public void startsDhcpServerOnNcm() throws Exception { + initStateMachine(TETHERING_NCM); + dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY); + dispatchTetherConnectionChanged(UPSTREAM_IFACE); + + assertDhcpStarted(new IpPrefix("192.168.42.0/24")); + } + + @Test + public void testOnNewPrefixRequest() throws Exception { + initStateMachine(TETHERING_NCM); + dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY); + + final IDhcpEventCallbacks eventCallbacks; + final ArgumentCaptor dhcpEventCbsCaptor = + ArgumentCaptor.forClass(IDhcpEventCallbacks.class); + verify(mDhcpServer, timeout(MAKE_DHCPSERVER_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), dhcpEventCbsCaptor.capture()); + eventCallbacks = dhcpEventCbsCaptor.getValue(); + assertDhcpStarted(new IpPrefix("192.168.42.0/24")); + + // Simulate the DHCP server receives DHCPDECLINE on MirrorLink and then signals + // onNewPrefixRequest callback. + eventCallbacks.onNewPrefixRequest(new IpPrefix("192.168.42.0/24")); + mLooper.dispatchAll(); + + final ArgumentCaptor lpCaptor = + ArgumentCaptor.forClass(LinkProperties.class); + InOrder inOrder = inOrder(mNetd, mCallback); + inOrder.verify(mCallback).updateInterfaceState( + mIpServer, STATE_LOCAL_ONLY, TETHER_ERROR_NO_ERROR); + inOrder.verify(mCallback).updateLinkProperties(eq(mIpServer), lpCaptor.capture()); + inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME); + // One for ipv4 route, one for ipv6 link local route. + inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME), + any(), any()); + inOrder.verify(mNetd).tetherApplyDnsInterfaces(); + inOrder.verify(mCallback).updateLinkProperties(eq(mIpServer), lpCaptor.capture()); + verifyNoMoreInteractions(mCallback); + + final LinkProperties linkProperties = lpCaptor.getValue(); + final List linkAddresses = linkProperties.getLinkAddresses(); + assertEquals(1, linkProperties.getLinkAddresses().size()); + assertEquals(1, linkProperties.getRoutes().size()); + final IpPrefix prefix = new IpPrefix(linkAddresses.get(0).getAddress(), + linkAddresses.get(0).getPrefixLength()); + assertNotEquals(prefix, new IpPrefix("192.168.42.0/24")); + + verify(mDhcpServer).updateParams(mDhcpParamsCaptor.capture(), any()); + assertDhcpServingParams(mDhcpParamsCaptor.getValue(), prefix); + } + @Test public void doesNotStartDhcpServerIfDisabled() throws Exception { initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, true /* usingLegacyDhcp */, @@ -731,19 +788,26 @@ public class IpServerTest { verify(mIpNeighborMonitor, never()).start(); } - 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( - any(), any()); - final DhcpServingParamsParcel params = mDhcpParamsCaptor.getValue(); + private void assertDhcpServingParams(final DhcpServingParamsParcel params, + final IpPrefix prefix) { // Last address byte is random - assertTrue(expectedPrefix.contains(intToInet4AddressHTH(params.serverAddr))); - assertEquals(expectedPrefix.getPrefixLength(), params.serverAddrPrefixLength); + assertTrue(prefix.contains(intToInet4AddressHTH(params.serverAddr))); + assertEquals(prefix.getPrefixLength(), params.serverAddrPrefixLength); assertEquals(1, params.defaultRouters.length); assertEquals(params.serverAddr, params.defaultRouters[0]); assertEquals(1, params.dnsServers.length); assertEquals(params.serverAddr, params.dnsServers[0]); assertEquals(DHCP_LEASE_TIME_SECS, params.dhcpLeaseTimeSecs); + if (mIpServer.interfaceType() == TETHERING_NCM) { + assertTrue(params.changePrefixOnDecline); + } + } + + 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( + any(), any()); + assertDhcpServingParams(mDhcpParamsCaptor.getValue(), expectedPrefix); } /**