| /* |
| * 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 preferredTimeout = 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) (preferredTimeout * 0.5); |
| } |
| if (rebindTimeout == 0) { |
| rebindTimeout = (int) (preferredTimeout * 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<IaPrefixOption> 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<IaPrefixOption> 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<IaPrefixOption> 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; |
| } |
| } |