/* * Copyright (C) 2023 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.dhcp6; import static android.net.dhcp6.Dhcp6Packet.IAID; import static android.net.dhcp6.Dhcp6Packet.PrefixDelegation; import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; import static android.system.OsConstants.AF_INET6; import static android.system.OsConstants.IPPROTO_UDP; import static android.system.OsConstants.SOCK_DGRAM; import static android.system.OsConstants.SOCK_NONBLOCK; import static com.android.net.module.util.NetworkStackConstants.ALL_DHCP_RELAY_AGENTS_AND_SERVERS; import static com.android.net.module.util.NetworkStackConstants.DHCP6_CLIENT_PORT; import static com.android.net.module.util.NetworkStackConstants.DHCP6_SERVER_PORT; import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY; import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH; import android.content.Context; import android.net.ip.IpClient; import android.net.util.SocketUtils; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.system.ErrnoException; import android.system.Os; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.util.HexDump; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.internal.util.WakeupMessage; import com.android.net.module.util.DeviceConfigUtils; import com.android.net.module.util.InterfaceParams; import com.android.net.module.util.PacketReader; import com.android.net.module.util.structs.IaPrefixOption; import java.io.FileDescriptor; import java.io.IOException; import java.net.SocketException; import java.nio.ByteBuffer; import java.util.Collections; import java.util.List; import java.util.Random; import java.util.function.IntSupplier; /** * A DHCPv6 client. * * So far only support IA_PD (prefix delegation), not for IA_NA/IA_TA yet. * * @hide */ public class Dhcp6Client extends StateMachine { private static final String TAG = Dhcp6Client.class.getSimpleName(); private static final boolean DBG = true; // Dhcp6Client shares the same handler with IpClient, define the base command range for // both public and private messages used in Dhcp6Client, to avoid commands overlap. // Public messages. private static final int PUBLIC_BASE = IpClient.DHCP6CLIENT_CMD_BASE; // Commands from controller to start/stop DHCPv6 public static final int CMD_START_DHCP6 = PUBLIC_BASE + 1; public static final int CMD_STOP_DHCP6 = PUBLIC_BASE + 2; // Notification from DHCPv6 state machine post DHCPv6 discovery/renewal. Indicates // success/failure public static final int CMD_DHCP6_RESULT = PUBLIC_BASE + 3; // Message.arg1 arguments to CMD_DHCP6_RESULT notification public static final int DHCP6_PD_SUCCESS = 1; public static final int DHCP6_PD_PREFIX_EXPIRED = 2; // Notification from DHCPv6 state machine before quitting public static final int CMD_ON_QUIT = PUBLIC_BASE + 4; // Internal messages. private static final int PRIVATE_BASE = IpClient.DHCP6CLIENT_CMD_BASE + 100; private static final int CMD_RECEIVED_PACKET = PRIVATE_BASE + 1; private static final int CMD_KICK = PRIVATE_BASE + 2; private static final int CMD_DHCP6_PD_RENEW = PRIVATE_BASE + 3; private static final int CMD_DHCP6_PD_REBIND = PRIVATE_BASE + 4; private static final int CMD_DHCP6_PD_EXPIRE = PRIVATE_BASE + 5; // Transmission and Retransmission parameters in milliseconds. private static final int SECONDS = 1000; private static final int SOL_TIMEOUT = 1 * SECONDS; private static final int SOL_MAX_RT = 3600 * SECONDS; private static final int REQ_TIMEOUT = 1 * SECONDS; private static final int REQ_MAX_RT = 30 * SECONDS; private static final int REQ_MAX_RC = 10; private static final int REN_TIMEOUT = 10 * SECONDS; private static final int REN_MAX_RT = 600 * SECONDS; private static final int REB_TIMEOUT = 10 * SECONDS; private static final int REB_MAX_RT = 600 * SECONDS; private int mSolMaxRtMs = SOL_MAX_RT; @Nullable private PrefixDelegation mAdvertise; @Nullable private PrefixDelegation mReply; @Nullable private byte[] mServerDuid; // State variables. @NonNull private final Dependencies mDependencies; @NonNull private final Context mContext; @NonNull private final Random mRandom; @NonNull private final StateMachine mController; @NonNull private final WakeupMessage mKickAlarm; @NonNull private final WakeupMessage mRenewAlarm; @NonNull private final WakeupMessage mRebindAlarm; @NonNull private final WakeupMessage mExpiryAlarm; @NonNull private final InterfaceParams mIface; @NonNull private final Dhcp6PacketHandler mDhcp6PacketHandler; @NonNull private final byte[] mClientDuid; // States. private State mStoppedState = new StoppedState(); private State mStartedState = new StartedState(); private State mSolicitState = new SolicitState(); private State mRequestState = new RequestState(); private State mHaveLeaseState = new HaveLeaseState(); private State mBoundState = new BoundState(); private State mRenewState = new RenewState(); private State mRebindState = new RebindState(); /** * Encapsulates Dhcp6Client depencencies that's used for unit testing and * integration testing. */ public static class Dependencies { /** * Read an integer DeviceConfig property. */ public int getDeviceConfigPropertyInt(String name, int defaultValue) { return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY, name, defaultValue); } } private WakeupMessage makeWakeupMessage(String cmdName, int cmd) { cmdName = Dhcp6Client.class.getSimpleName() + "." + mIface.name + "." + cmdName; return new WakeupMessage(mContext, getHandler(), cmdName, cmd); } private Dhcp6Client(@NonNull final Context context, @NonNull final StateMachine controller, @NonNull final InterfaceParams iface, @NonNull final Dependencies deps) { super(TAG, controller.getHandler()); mDependencies = deps; mContext = context; mController = controller; mIface = iface; mClientDuid = Dhcp6Packet.createClientDuid(iface.macAddr); mDhcp6PacketHandler = new Dhcp6PacketHandler(getHandler()); addState(mStoppedState); addState(mStartedState); { addState(mSolicitState, mStartedState); addState(mRequestState, mStartedState); addState(mHaveLeaseState, mStartedState); { addState(mBoundState, mHaveLeaseState); addState(mRenewState, mHaveLeaseState); addState(mRebindState, mHaveLeaseState); } } setInitialState(mStoppedState); mRandom = new Random(); // Used to schedule packet retransmissions. mKickAlarm = makeWakeupMessage("KICK", CMD_KICK); // Used to schedule DHCP reacquisition. mRenewAlarm = makeWakeupMessage("RENEW", CMD_DHCP6_PD_RENEW); mRebindAlarm = makeWakeupMessage("REBIND", CMD_DHCP6_PD_REBIND); mExpiryAlarm = makeWakeupMessage("EXPIRY", CMD_DHCP6_PD_EXPIRE); } /** * Make a Dhcp6Client instance. */ public static Dhcp6Client makeDhcp6Client(@NonNull final Context context, @NonNull final StateMachine controller, @NonNull final InterfaceParams ifParams, @NonNull final Dependencies deps) { final Dhcp6Client client = new Dhcp6Client(context, controller, ifParams, deps); client.start(); return client; } /** * Quit the Dhcp6 StateMachine. * * @hide */ public void doQuit() { Log.d(TAG, "doQuit"); quit(); } @Override protected void onQuitting() { Log.d(TAG, "onQuitting"); mController.sendMessage(CMD_ON_QUIT); } /** * Retransmits packets per algorithm defined in RFC8415 section 15. Packet transmission is * triggered by CMD_KICK, which is sent by an AlarmManager alarm. Kicks are cancelled when * leaving the state. * * Concrete subclasses must initialize retransmission parameters and implement sendPacket, * which is called when the alarm fires and a packet needs to be transmitted, and receivePacket, * which is triggered by CMD_RECEIVED_PACKET sent by the receive thread. */ abstract class MessageExchangeState extends State { private int mTransId = 0; private long mTransStartMs = 0; private long mMaxRetransTimeMs = 0; private long mRetransTimeout = -1; private int mRetransCount = 0; private final long mInitialDelayMs; private final long mInitialRetransTimeMs; private final int mMaxRetransCount; private final IntSupplier mMaxRetransTimeSupplier; MessageExchangeState(final int delay, final int irt, final int mrc, final IntSupplier mrt) { mInitialDelayMs = delay; mInitialRetransTimeMs = irt; mMaxRetransCount = mrc; mMaxRetransTimeSupplier = mrt; } @Override public void enter() { super.enter(); mMaxRetransTimeMs = mMaxRetransTimeSupplier.getAsInt(); // Every message exchange generates a new transaction id. mTransId = mRandom.nextInt() & 0xffffff; sendMessageDelayed(CMD_KICK, mInitialDelayMs); } private void handleKick() { // rfc8415#section-21.9: The elapsed time is measured from the time at which the // client sent the first message in the message exchange, and the elapsed-time field // is set to 0 in the first message in the message exchange. final long elapsedTimeMs; if (mRetransCount == 0) { elapsedTimeMs = 0; mTransStartMs = SystemClock.elapsedRealtime(); } else { elapsedTimeMs = SystemClock.elapsedRealtime() - mTransStartMs; } sendPacket(mTransId, elapsedTimeMs); // Compares retransmission parameters and reschedules alarm accordingly. scheduleKick(); } private void handleReceivedPacket(@NonNull final Dhcp6Packet packet) { // Technically it is valid for the server to not include a prefix in an IA in certain // scenarios (specifically in a reply to Renew / Rebind, which means: do not extend the // prefix, e.g. the list of prefix is empty). However, if prefix(es) do exist and all // prefixes are invalid, then we should just ignore this packet. if (!packet.isValid(mTransId, mClientDuid)) return; if (!packet.mPrefixDelegation.ipos.isEmpty()) { boolean allInvalidPrefixes = true; for (IaPrefixOption ipo : packet.mPrefixDelegation.ipos) { if (ipo != null && ipo.isValid()) { allInvalidPrefixes = false; break; } } if (allInvalidPrefixes) { Log.w(TAG, "All IA_Prefix options included in the " + packet.getClass().getSimpleName() + " are invalid, ignore it."); return; } } receivePacket(packet); } @Override public boolean processMessage(Message message) { if (super.processMessage(message) == HANDLED) { return HANDLED; } switch (message.what) { case CMD_KICK: handleKick(); return HANDLED; case CMD_RECEIVED_PACKET: handleReceivedPacket((Dhcp6Packet) message.obj); return HANDLED; default: return NOT_HANDLED; } } @Override public void exit() { super.exit(); mKickAlarm.cancel(); mRetransTimeout = -1; mRetransCount = 0; mMaxRetransTimeMs = 0; } protected abstract boolean sendPacket(int transId, long elapsedTimeMs); protected abstract void receivePacket(Dhcp6Packet packet); // If the message exchange is considered to have failed according to the retransmission // mechanism(i.e. client has transmitted the message MRC times or MRD seconds has elapsed // since the first message transmission), this method will be called to roll back to Solicit // state and restart the configuration, and notify IpClient the DHCPv6 message exchange // failure if needed. protected void onMessageExchangeFailed() {} /** * Per RFC8415 section 15, each of the computations of a new RT includes a randomization * factor (RAND), which is a random number chosen with a uniform distribution between -0.1 * and +0.1. */ private double rand() { return mRandom.nextDouble() / 5 - 0.1; } protected void scheduleKick() { if (mRetransTimeout == -1) { // RT for the first message transmission is based on IRT. mRetransTimeout = mInitialRetransTimeMs + (long) (rand() * mInitialRetransTimeMs); } else { // RT for each subsequent message transmission is based on the previous value of RT. mRetransTimeout = 2 * mRetransTimeout + (long) (rand() * mRetransTimeout); } if (mMaxRetransTimeMs != 0 && mRetransTimeout > mMaxRetransTimeMs) { mRetransTimeout = mMaxRetransTimeMs + (long) (rand() * mMaxRetransTimeMs); } // Per RFC8415 section 18.2.4 and 18.2.5, MRD equals to the remaining time until // earliest T2(RenewState) or valid lifetimes of all leases in all IA have expired // (RebindState), and message exchange is terminated when the earliest time T2 is // reached, at which point client begins the Rebind message exchange, however, section // 15 says the message exchange fails(terminated) once MRD seconds have elapsed since // the client first transmitted the message. So far MRD is being used for Renew, Rebind // and Confirm message retransmission. Given we don't support Confirm message yet, we // can just use rebindTimeout and expirationTimeout on behalf of MRD which have been // scheduled in BoundState to simplify the implementation, therefore, we don't need to // explicitly assign the MRD in the subclasses. if (mMaxRetransCount != 0 && mRetransCount > mMaxRetransCount) { onMessageExchangeFailed(); Log.i(TAG, "client has transmitted the message " + mMaxRetransCount + " times, stopping retransmission"); return; } mKickAlarm.schedule(SystemClock.elapsedRealtime() + mRetransTimeout); mRetransCount++; } } private void scheduleLeaseTimers() { // TODO: validate t1, t2, valid and preferred lifetimes before the timers are scheduled // to prevent packet storms due to low timeouts. Preferred/valid lifetime of 0 should be // excluded before scheduling the lease timer. int renewTimeout = mReply.t1; int rebindTimeout = mReply.t2; final long deprecationTimeout = mReply.getMinimalPreferredLifetime(); final long expirationTimeout = mReply.getMinimalValidLifetime(); // rfc8415#section-14.2: if t1 and / or t2 are 0, the client chooses an appropriate value. // rfc8415#section-21.21: Recommended values for T1 and T2 are 0.5 and 0.8 times the // shortest preferred lifetime of the prefixes in the IA_PD that the server is willing to // extend, respectively. if (renewTimeout == 0) { renewTimeout = (int) (deprecationTimeout * 0.5); } if (rebindTimeout == 0) { rebindTimeout = (int) (deprecationTimeout * 0.8); } // Note: message validation asserts that the received t1 <= t2 if both t1 > 0 and t2 > 0. // However, if t1 or t2 are 0, it is possible for renewTimeout to become larger than // rebindTimeout (and similarly, rebindTimeout to become larger than expirationTimeout). // For example: t1 = 0, t2 = 40, valid lft = 100 results in renewTimeout = 50, and // rebindTimeout = 40. Hence, their correct order must be asserted below. // If timeouts happen to coincide or are out of order, the former (in respect to the // specified provisioning lifecycle) can be skipped. This also takes care of the case where // the server sets t1 == t2 == valid lft, which indicates that the IA cannot be renewed, so // there is no point in trying. if (renewTimeout >= rebindTimeout) { // skip RENEW renewTimeout = 0; } if (rebindTimeout >= expirationTimeout) { // skip REBIND rebindTimeout = 0; } final long now = SystemClock.elapsedRealtime(); if (renewTimeout > 0) { mRenewAlarm.schedule(now + renewTimeout * (long) SECONDS); Log.d(TAG, "Scheduling IA_PD renewal in " + renewTimeout + "s"); } if (rebindTimeout > 0) { mRebindAlarm.schedule(now + rebindTimeout * (long) SECONDS); Log.d(TAG, "Scheduling IA_PD rebind in " + rebindTimeout + "s"); } mExpiryAlarm.schedule(now + expirationTimeout * (long) SECONDS); Log.d(TAG, "Scheduling IA_PD expiry in " + expirationTimeout + "s"); } private void notifyPrefixDelegation(int result, @Nullable final List ipos) { mController.sendMessage(CMD_DHCP6_RESULT, result, 0, ipos); } private void clearDhcp6State() { mAdvertise = null; mReply = null; mServerDuid = null; mSolMaxRtMs = SOL_MAX_RT; } @SuppressWarnings("ByteBufferBackingArray") private boolean sendSolicitPacket(int transId, long elapsedTimeMs, final ByteBuffer iapd) { final ByteBuffer packet = Dhcp6Packet.buildSolicitPacket(transId, elapsedTimeMs, iapd.array(), mClientDuid, true /* rapidCommit */); return transmitPacket(packet, "solicit"); } @SuppressWarnings("ByteBufferBackingArray") private boolean sendRequestPacket(int transId, long elapsedTimeMs, final ByteBuffer iapd) { final ByteBuffer packet = Dhcp6Packet.buildRequestPacket(transId, elapsedTimeMs, iapd.array(), mClientDuid, mServerDuid); return transmitPacket(packet, "request"); } @SuppressWarnings("ByteBufferBackingArray") private boolean sendRenewPacket(int transId, long elapsedTimeMs, final ByteBuffer iapd) { final ByteBuffer packet = Dhcp6Packet.buildRenewPacket(transId, elapsedTimeMs, iapd.array(), mClientDuid, mServerDuid); return transmitPacket(packet, "renew"); } @SuppressWarnings("ByteBufferBackingArray") private boolean sendRebindPacket(int transId, long elapsedTimeMs, final ByteBuffer iapd) { final ByteBuffer packet = Dhcp6Packet.buildRebindPacket(transId, elapsedTimeMs, iapd.array(), mClientDuid); return transmitPacket(packet, "rebind"); } /** * Parent state at which client does initialization of interface and packet handler, also * processes the CMD_STOP_DHCP6 command in this state which child states don't handle. */ class StartedState extends State { @Override public void enter() { clearDhcp6State(); if (mDhcp6PacketHandler.start()) return; Log.e(TAG, "Fail to start DHCPv6 Packet Handler"); // We cannot call transitionTo because a transition is still in progress. // Instead, ensure that we process CMD_STOP_DHCP6 as soon as the transition is complete. deferMessage(obtainMessage(CMD_STOP_DHCP6)); } @Override public void exit() { mDhcp6PacketHandler.stop(); if (DBG) Log.d(TAG, "DHCPv6 Packet Handler stopped"); clearDhcp6State(); } @Override public boolean processMessage(Message message) { super.processMessage(message); switch (message.what) { case CMD_STOP_DHCP6: transitionTo(mStoppedState); return HANDLED; default: return NOT_HANDLED; } } } /** * Initial state of DHCPv6 state machine. */ class StoppedState extends State { @Override public boolean processMessage(Message message) { switch (message.what) { case CMD_START_DHCP6: // TODO: store the delegated prefix in IpMemoryStore and start in REBIND instead // of SOLICIT if there is already a valid prefix on this network. transitionTo(mSolicitState); return HANDLED; default: return NOT_HANDLED; } } } /** * Client (re)transmits a Solicit message to locate DHCPv6 servers and processes the Advertise * message in this state. * * Note: Not implement DHCPv6 server selection, always request the first Advertise we receive. */ class SolicitState extends MessageExchangeState { SolicitState() { // First Solicit message should be delayed by a random amount of time between 0 // and SOL_MAX_DELAY(1s). super((int) (new Random().nextDouble() * SECONDS) /* delay */, SOL_TIMEOUT /* IRT */, 0 /* MRC */, () -> mSolMaxRtMs /* MRT */); } @Override public void enter() { super.enter(); } @Override protected boolean sendPacket(int transId, long elapsedTimeMs) { final IaPrefixOption hintOption = new IaPrefixOption((short) IaPrefixOption.LENGTH, 0 /* preferred */, 0 /* valid */, (byte) RFC7421_PREFIX_LENGTH, new byte[16] /* empty prefix */); final PrefixDelegation pd = new PrefixDelegation(IAID, 0 /* t1 */, 0 /* t2 */, Collections.singletonList(hintOption)); return sendSolicitPacket(transId, elapsedTimeMs, pd.build()); } @Override protected void receivePacket(Dhcp6Packet packet) { final PrefixDelegation pd = packet.mPrefixDelegation; // Ignore any Advertise or Reply for Solicit(with Rapid Commit) with NoPrefixAvail // status code, retransmit Solicit to see if any valid response from other Servers. if (pd.statusCode == Dhcp6Packet.STATUS_NO_PREFIX_AVAIL) { Log.w(TAG, "Server responded to Solicit without available prefix, ignoring"); return; } if (packet instanceof Dhcp6AdvertisePacket) { Log.d(TAG, "Get prefix delegation option from Advertise: " + pd); mAdvertise = pd; mServerDuid = packet.mServerDuid; mSolMaxRtMs = packet.getSolMaxRtMs().orElse(mSolMaxRtMs); transitionTo(mRequestState); } else if (packet instanceof Dhcp6ReplyPacket) { if (!packet.mRapidCommit) { Log.e(TAG, "Server responded to Solicit with Reply without rapid commit option" + ", ignoring"); return; } Log.d(TAG, "Get prefix delegation option from RapidCommit Reply: " + pd); mReply = pd; mServerDuid = packet.mServerDuid; mSolMaxRtMs = packet.getSolMaxRtMs().orElse(mSolMaxRtMs); transitionTo(mBoundState); } } } /** * Client (re)transmits a Request message to request configuration from a specific server and * process the Reply message in this state. */ class RequestState extends MessageExchangeState { RequestState() { super(0 /* delay */, REQ_TIMEOUT /* IRT */, REQ_MAX_RC /* MRC */, () -> REQ_MAX_RT /* MRT */); } @Override protected boolean sendPacket(int transId, long elapsedTimeMs) { return sendRequestPacket(transId, elapsedTimeMs, mAdvertise.build()); } @Override protected void receivePacket(Dhcp6Packet packet) { if (!(packet instanceof Dhcp6ReplyPacket)) return; final PrefixDelegation pd = packet.mPrefixDelegation; if (pd.statusCode == Dhcp6Packet.STATUS_NO_PREFIX_AVAIL) { Log.w(TAG, "Server responded to Request without available prefix, restart Solicit"); transitionTo(mSolicitState); return; } Log.d(TAG, "Get prefix delegation option from Reply: " + pd); mReply = pd; mSolMaxRtMs = packet.getSolMaxRtMs().orElse(mSolMaxRtMs); transitionTo(mBoundState); } @Override protected void onMessageExchangeFailed() { transitionTo(mSolicitState); } } /** * Parent state of other states at which client has already obtained the lease from server. */ class HaveLeaseState extends State { @Override public boolean processMessage(Message message) { switch (message.what) { case CMD_DHCP6_PD_EXPIRE: notifyPrefixDelegation(DHCP6_PD_PREFIX_EXPIRED, mReply.getValidIaPrefixes()); transitionTo(mSolicitState); return HANDLED; default: return NOT_HANDLED; } } @Override public void exit() { // Clear any extant alarms. mRenewAlarm.cancel(); mRebindAlarm.cancel(); mExpiryAlarm.cancel(); clearDhcp6State(); } } /** * Client has already obtained the lease(e.g. IA_PD option) from server and stays in Bound * state until T1 expires, and then transition to Renew state to extend the lease duration. */ class BoundState extends State { @Override public void enter() { super.enter(); scheduleLeaseTimers(); // Pass valid delegated prefix(es) to IpClient for IPv6 address configuration and // active prefix(es) maintenance. notifyPrefixDelegation(DHCP6_PD_SUCCESS, mReply.getValidIaPrefixes()); } @Override public boolean processMessage(Message message) { super.processMessage(message); switch (message.what) { case CMD_DHCP6_PD_RENEW: transitionTo(mRenewState); return HANDLED; default: return NOT_HANDLED; } } } /** * Per RFC8415 section 18.2.10.1: Reply for renew or Rebind. * - If all binding IA_PDs were renewed/rebound(so far we only support one IA_PD option per * interface), then move to BoundState to update the existing global IPv6 addresses lifetime * or install new global IPv6 address depending on the response from server. * - Server may add new IA prefix option in Reply message(e.g. due to renumbering events), or * may choose to deprecate some prefixes if it cannot extend the lifetime by: * - either not including these requested IA prefixes in Reply message * - or setting the valid lifetime equals to T1/T2 * That forces previous delegated prefixes to expire in a natural way, and client should * also stop trying to extend the lifetime for them. That being said, the global IPv6 address * lifetime won't be updated in BoundState if corresponding prefix doesn't appear in Reply * message, resulting in these global IPv6 addresses expire eventually and IpClient obtains * these updates via netlink message and remove the delegated prefix(es) from LinkProperties. * - If some binding IA_PDs were absent in Reply message, client should still stay at RenewState * or RebindState and retransmit Renew/Rebind messages to see if it can get all later. So far * we only support one IA_PD option per interface, if the received Reply message doesn't take * any IA_Prefix option, then treat it as if IA_PD is absent, since there's no point in * returning BoundState again. */ abstract class ReacquireState extends MessageExchangeState { ReacquireState(final int irt, final int mrt) { super(0 /* delay */, irt, 0 /* MRC */, () -> mrt /* MRT */); } @Override public void enter() { super.enter(); } @Override protected void receivePacket(Dhcp6Packet packet) { if (!(packet instanceof Dhcp6ReplyPacket)) return; final PrefixDelegation pd = packet.mPrefixDelegation; // Stay at Renew/Rebind state if the Reply message takes NoPrefixAvail status code, // retransmit Renew/Rebind message to server, to retry obtaining the prefixes. if (pd.statusCode == Dhcp6Packet.STATUS_NO_PREFIX_AVAIL) { Log.w(TAG, "Server responded to Renew/Rebind without available prefix, ignoring"); return; } // TODO: send a Request message to the server that responded if any of the IA_PDs in // Reply message contain NoBinding status code. Log.d(TAG, "Get prefix delegation option from Reply as response to Renew/Rebind " + pd); if (pd.ipos.isEmpty()) return; mReply = pd; mServerDuid = packet.mServerDuid; // Once the delegated prefix gets refreshed successfully we have to extend the // preferred lifetime and valid lifetime of global IPv6 addresses, otherwise // these addresses will become depreacated finally and then provisioning failure // happens. So we transit to mBoundState to update the address with refreshed // preferred and valid lifetime via sending RTM_NEWADDR message, going back to // Bound state after a success update. transitionTo(mBoundState); } } /** * Client enters Renew state when T1 expires and (re)transmits Renew message to the * server that originally provided the client's leases and configuration parameters to * extend the lifetimes on the leases assigned to the client. */ class RenewState extends ReacquireState { RenewState() { super(REN_TIMEOUT, REN_MAX_RT); } @Override public boolean processMessage(Message message) { if (super.processMessage(message) == HANDLED) { return HANDLED; } switch (message.what) { case CMD_DHCP6_PD_REBIND: transitionTo(mRebindState); return HANDLED; default: return NOT_HANDLED; } } @Override protected boolean sendPacket(int transId, long elapsedTimeMs) { final List toBeRenewed = mReply.getRenewableIaPrefixes(); if (toBeRenewed.isEmpty()) { if (DBG) Log.d(TAG, "Do not send Renew message due to no renewable prefix."); return false; } return sendRenewPacket(transId, elapsedTimeMs, mReply.build(toBeRenewed)); } } /** * Client enters Rebind state when T2 expires and (re)transmits Rebind message to any * available server to extend the lifetimes on the leases assigned to the client and to * update other configuration parameters. */ class RebindState extends ReacquireState { RebindState() { super(REB_TIMEOUT, REB_MAX_RT); } @Override protected boolean sendPacket(int transId, long elapsedTimeMs) { final List toBeRebound = mReply.getRenewableIaPrefixes(); if (toBeRebound.isEmpty()) { if (DBG) Log.d(TAG, "Do not send Rebind message due to no renewable prefix."); return false; } return sendRebindPacket(transId, elapsedTimeMs, mReply.build(toBeRebound)); } } private class Dhcp6PacketHandler extends PacketReader { private FileDescriptor mUdpSock; Dhcp6PacketHandler(Handler handler) { super(handler); } @Override protected void handlePacket(byte[] recvbuf, int length) { try { final Dhcp6Packet packet = Dhcp6Packet.decode(recvbuf, length); if (DBG) Log.d(TAG, "Received packet: " + packet); sendMessage(CMD_RECEIVED_PACKET, packet); } catch (Dhcp6Packet.ParseException e) { Log.e(TAG, "Can't parse DHCPv6 packet: " + e.getMessage()); } } @Override protected FileDescriptor createFd() { try { mUdpSock = Os.socket(AF_INET6, SOCK_DGRAM | SOCK_NONBLOCK, IPPROTO_UDP); SocketUtils.bindSocketToInterface(mUdpSock, mIface.name); Os.bind(mUdpSock, IPV6_ADDR_ANY, DHCP6_CLIENT_PORT); } catch (SocketException | ErrnoException e) { Log.e(TAG, "Error creating udp socket", e); closeFd(mUdpSock); mUdpSock = null; return null; } return mUdpSock; } public int transmitPacket(final ByteBuffer buf) throws ErrnoException, SocketException { int ret = Os.sendto(mUdpSock, buf.array(), 0 /* byteOffset */, buf.limit() /* byteCount */, 0 /* flags */, ALL_DHCP_RELAY_AGENTS_AND_SERVERS, DHCP6_SERVER_PORT); return ret; } } @SuppressWarnings("ByteBufferBackingArray") private boolean transmitPacket(@NonNull final ByteBuffer buf, @NonNull final String description) { try { if (DBG) { Log.d(TAG, "Multicasting " + description + " to ff02::1:2" + " packet raw data: " + HexDump.toHexString(buf.array(), 0, buf.limit())); } mDhcp6PacketHandler.transmitPacket(buf); } catch (ErrnoException | IOException e) { Log.e(TAG, "Can't send packet: ", e); return false; } return true; } }