summaryrefslogtreecommitdiff
path: root/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java
blob: 36192537493a80a55ccfa8bc02662ec062c28d57 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
/*
 * 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 com.android.server.vcn.routeselection;

import static com.android.internal.annotations.VisibleForTesting.Visibility;
import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.IpSecTransformState;
import android.net.Network;
import android.net.vcn.Flags;
import android.net.vcn.VcnManager;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.OutcomeReceiver;
import android.os.PowerManager;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting.Visibility;
import com.android.server.vcn.VcnContext;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.BitSet;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * IpSecPacketLossDetector is responsible for continuously monitoring IPsec packet loss
 *
 * <p>When the packet loss rate surpass the threshold, IpSecPacketLossDetector will report it to the
 * caller
 *
 * <p>IpSecPacketLossDetector will start monitoring when the network being monitored is selected AND
 * an inbound IpSecTransform has been applied to this network.
 *
 * <p>This class is flag gated by "network_metric_monitor" and "ipsec_tramsform_state"
 */
public class IpSecPacketLossDetector extends NetworkMetricMonitor {
    private static final String TAG = IpSecPacketLossDetector.class.getSimpleName();

    private static final int PACKET_LOSS_PERCENT_UNAVAILABLE = -1;

    // Ignore the packet loss detection result if the expected packet number is smaller than 10.
    // Solarwinds NPM uses 10 ICMP echos to calculate packet loss rate (as per
    // https://thwack.solarwinds.com/products/network-performance-monitor-npm/f/forum/63829/how-is-packet-loss-calculated)
    @VisibleForTesting(visibility = Visibility.PRIVATE)
    static final int MIN_VALID_EXPECTED_RX_PACKET_NUM = 10;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef(
            prefix = {"PACKET_LOSS_"},
            value = {
                PACKET_LOSS_RATE_VALID,
                PACKET_LOSS_RATE_INVALID,
                PACKET_LOSS_UNUSUAL_SEQ_NUM_LEAP,
            })
    @Target({ElementType.TYPE_USE})
    private @interface PacketLossResultType {}

    /** Indicates a valid packet loss rate is available */
    private static final int PACKET_LOSS_RATE_VALID = 0;

    /**
     * Indicates that the detector cannot get a valid packet loss rate due to one of the following
     * reasons:
     *
     * <ul>
     *   <li>The replay window did not proceed and thus all packets might have been delivered out of
     *       order
     *   <li>The expected received packet number is too small and thus the detection result is not
     *       reliable
     *   <li>There are unexpected errors
     * </ul>
     */
    private static final int PACKET_LOSS_RATE_INVALID = 1;

    /**
     * The sequence number increase is unusually large and might be caused an intentional leap on
     * the server's downlink
     *
     * <p>Inbound sequence number will not always increase consecutively. During load balancing the
     * server might add a big leap on the sequence number intentionally. In such case a high packet
     * loss rate does not always indicate a lossy network
     */
    private static final int PACKET_LOSS_UNUSUAL_SEQ_NUM_LEAP = 2;

    // For VoIP, losses between 5% and 10% of the total packet stream will affect the quality
    // significantly (as per "Computer Networking for LANS to WANS: Hardware, Software and
    // Security"). For audio and video streaming, above 10-12% packet loss is unacceptable (as per
    // "ICTP-SDU: About PingER"). Thus choose 12% as a conservative default threshold to declare a
    // validation failure.
    private static final int IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DEFAULT = 12;

    private static final int POLL_IPSEC_STATE_INTERVAL_SECONDS_DEFAULT = 20;

    // By default, there's no maximum limit enforced
    private static final int MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED = -1;

    private long mPollIpSecStateIntervalMs;
    private int mPacketLossRatePercentThreshold;
    private int mMaxSeqNumIncreasePerSecond;

    @NonNull private final Handler mHandler;
    @NonNull private final PowerManager mPowerManager;
    @NonNull private final ConnectivityManager mConnectivityManager;
    @NonNull private final Object mCancellationToken = new Object();
    @NonNull private final PacketLossCalculator mPacketLossCalculator;

    @Nullable private IpSecTransformWrapper mInboundTransform;
    @Nullable private IpSecTransformState mLastIpSecTransformState;

    @VisibleForTesting(visibility = Visibility.PRIVATE)
    public IpSecPacketLossDetector(
            @NonNull VcnContext vcnContext,
            @NonNull Network network,
            @Nullable PersistableBundleWrapper carrierConfig,
            @NonNull NetworkMetricMonitorCallback callback,
            @NonNull Dependencies deps)
            throws IllegalAccessException {
        super(vcnContext, network, carrierConfig, callback);

        Objects.requireNonNull(deps, "Missing deps");

        if (!vcnContext.isFlagIpSecTransformStateEnabled()) {
            // Caller error
            logWtf("ipsecTransformState flag disabled");
            throw new IllegalAccessException("ipsecTransformState flag disabled");
        }

        mHandler = new Handler(getVcnContext().getLooper());

        mPowerManager = getVcnContext().getContext().getSystemService(PowerManager.class);
        mConnectivityManager =
                getVcnContext().getContext().getSystemService(ConnectivityManager.class);

        mPacketLossCalculator = deps.getPacketLossCalculator();

        mPollIpSecStateIntervalMs = getPollIpSecStateIntervalMs(carrierConfig);
        mPacketLossRatePercentThreshold = getPacketLossRatePercentThreshold(carrierConfig);
        mMaxSeqNumIncreasePerSecond = getMaxSeqNumIncreasePerSecond(carrierConfig);

        // Register for system broadcasts to monitor idle mode change
        final IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
        getVcnContext()
                .getContext()
                .registerReceiver(
                        new BroadcastReceiver() {
                            @Override
                            public void onReceive(Context context, Intent intent) {
                                if (PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED.equals(
                                                intent.getAction())
                                        && mPowerManager.isDeviceIdleMode()) {
                                    mLastIpSecTransformState = null;
                                }
                            }
                        },
                        intentFilter,
                        null /* broadcastPermission not required */,
                        mHandler);
    }

    public IpSecPacketLossDetector(
            @NonNull VcnContext vcnContext,
            @NonNull Network network,
            @Nullable PersistableBundleWrapper carrierConfig,
            @NonNull NetworkMetricMonitorCallback callback)
            throws IllegalAccessException {
        this(vcnContext, network, carrierConfig, callback, new Dependencies());
    }

    @VisibleForTesting(visibility = Visibility.PRIVATE)
    public static class Dependencies {
        public PacketLossCalculator getPacketLossCalculator() {
            return new PacketLossCalculator();
        }
    }

    private static long getPollIpSecStateIntervalMs(
            @Nullable PersistableBundleWrapper carrierConfig) {
        final int seconds;

        if (carrierConfig != null) {
            seconds =
                    carrierConfig.getInt(
                            VcnManager.VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY,
                            POLL_IPSEC_STATE_INTERVAL_SECONDS_DEFAULT);
        } else {
            seconds = POLL_IPSEC_STATE_INTERVAL_SECONDS_DEFAULT;
        }

        return TimeUnit.SECONDS.toMillis(seconds);
    }

    private static int getPacketLossRatePercentThreshold(
            @Nullable PersistableBundleWrapper carrierConfig) {
        if (carrierConfig != null) {
            return carrierConfig.getInt(
                    VcnManager.VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY,
                    IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DEFAULT);
        }
        return IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DEFAULT;
    }

    @VisibleForTesting(visibility = Visibility.PRIVATE)
    static int getMaxSeqNumIncreasePerSecond(@Nullable PersistableBundleWrapper carrierConfig) {
        int maxSeqNumIncrease = MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED;
        if (Flags.handleSeqNumLeap() && carrierConfig != null) {
            maxSeqNumIncrease =
                    carrierConfig.getInt(
                            VcnManager.VCN_NETWORK_SELECTION_MAX_SEQ_NUM_INCREASE_PER_SECOND_KEY,
                            MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED);
        }

        if (maxSeqNumIncrease < MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED) {
            logE(TAG, "Invalid value of MAX_SEQ_NUM_INCREASE_PER_SECOND_KEY " + maxSeqNumIncrease);
            return MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED;
        }

        return maxSeqNumIncrease;
    }

    @Override
    protected void onSelectedUnderlyingNetworkChanged() {
        if (!isSelectedUnderlyingNetwork()) {
            mInboundTransform = null;
            stop();
        }

        // No action when the underlying network got selected. Wait for the inbound transform to
        // start the monitor
    }

    @Override
    public void setInboundTransformInternal(@NonNull IpSecTransformWrapper inboundTransform) {
        Objects.requireNonNull(inboundTransform, "inboundTransform is null");

        if (Objects.equals(inboundTransform, mInboundTransform)) {
            return;
        }

        if (!isSelectedUnderlyingNetwork()) {
            logWtf("setInboundTransform called but network not selected");
            return;
        }

        // When multiple parallel inbound transforms are created, NetworkMetricMonitor will be
        // enabled on the last one as a sample
        mInboundTransform = inboundTransform;
        start();
    }

    @Override
    public void setCarrierConfig(@Nullable PersistableBundleWrapper carrierConfig) {
        // The already scheduled event will not be affected. The followup events will be scheduled
        // with the new interval
        mPollIpSecStateIntervalMs = getPollIpSecStateIntervalMs(carrierConfig);

        if (Flags.handleSeqNumLeap()) {
            mPacketLossRatePercentThreshold = getPacketLossRatePercentThreshold(carrierConfig);
            mMaxSeqNumIncreasePerSecond = getMaxSeqNumIncreasePerSecond(carrierConfig);
        }
    }

    @Override
    public void onLinkPropertiesOrCapabilitiesChanged() {
        if (!isStarted()) return;

        reschedulePolling();
    }

    private void reschedulePolling() {
        mHandler.removeCallbacksAndEqualMessages(mCancellationToken);
        mHandler.postDelayed(new PollIpSecStateRunnable(), mCancellationToken, 0L);
    }

    @Override
    protected void start() {
        super.start();
        clearTransformStateAndPollingEvents();
        mHandler.postDelayed(new PollIpSecStateRunnable(), mCancellationToken, 0L);
    }

    @Override
    public void stop() {
        super.stop();
        clearTransformStateAndPollingEvents();
    }

    private void clearTransformStateAndPollingEvents() {
        mHandler.removeCallbacksAndEqualMessages(mCancellationToken);
        mLastIpSecTransformState = null;
    }

    @Override
    public void close() {
        super.close();

        if (mInboundTransform != null) {
            mInboundTransform.close();
        }
    }

    @VisibleForTesting(visibility = Visibility.PRIVATE)
    @Nullable
    public IpSecTransformState getLastTransformState() {
        return mLastIpSecTransformState;
    }

    @VisibleForTesting(visibility = Visibility.PROTECTED)
    @Nullable
    public IpSecTransformWrapper getInboundTransformInternal() {
        return mInboundTransform;
    }

    private class PollIpSecStateRunnable implements Runnable {
        @Override
        public void run() {
            if (!isStarted()) {
                logWtf("Monitor stopped but PollIpSecStateRunnable not removed from Handler");
                return;
            }

            getInboundTransformInternal()
                    .requestIpSecTransformState(
                            new HandlerExecutor(mHandler), new IpSecTransformStateReceiver());

            // Schedule for next poll
            mHandler.postDelayed(
                    new PollIpSecStateRunnable(), mCancellationToken, mPollIpSecStateIntervalMs);
        }
    }

    private class IpSecTransformStateReceiver
            implements OutcomeReceiver<IpSecTransformState, RuntimeException> {
        @Override
        public void onResult(@NonNull IpSecTransformState state) {
            getVcnContext().ensureRunningOnLooperThread();

            if (!isStarted()) {
                return;
            }

            onIpSecTransformStateReceived(state);
        }

        @Override
        public void onError(@NonNull RuntimeException error) {
            getVcnContext().ensureRunningOnLooperThread();

            // Nothing we can do here
            logW("TransformStateReceiver#onError " + error.toString());
        }
    }

    private void onIpSecTransformStateReceived(@NonNull IpSecTransformState state) {
        if (mLastIpSecTransformState == null) {
            // This is first time to poll the state
            mLastIpSecTransformState = state;
            return;
        }

        final PacketLossCalculationResult calculateResult =
                mPacketLossCalculator.getPacketLossRatePercentage(
                        mLastIpSecTransformState,
                        state,
                        mMaxSeqNumIncreasePerSecond,
                        getLogPrefix());

        if (calculateResult.getResultType() == PACKET_LOSS_RATE_INVALID) {
            return;
        }

        final String logMsg =
                "calculateResult: "
                        + calculateResult
                        + "% in the past "
                        + (state.getTimestampMillis()
                                - mLastIpSecTransformState.getTimestampMillis())
                        + "ms";

        mLastIpSecTransformState = state;
        if (calculateResult.getPacketLossRatePercent() < mPacketLossRatePercentThreshold) {
            logV(logMsg);

            // In both "valid" or "unusual_seq_num_leap" cases, notify that the network has passed
            // the validation
            onValidationResultReceivedInternal(false /* isFailed */);
        } else {
            logInfo(logMsg);

            if (calculateResult.getResultType() == PACKET_LOSS_RATE_VALID) {
                onValidationResultReceivedInternal(true /* isFailed */);
            }

            // In both "valid" or "unusual_seq_num_leap" cases, trigger network validation
            if (Flags.validateNetworkOnIpsecLoss()) {
                // Trigger re-validation of the underlying network; if it fails, the VCN will
                // attempt to migrate away.
                mConnectivityManager.reportNetworkConnectivity(
                        getNetwork(), false /* hasConnectivity */);
            }
        }
    }

    @VisibleForTesting(visibility = Visibility.PRIVATE)
    public static class PacketLossCalculator {
        /** Calculate the packet loss rate between two timestamps */
        public PacketLossCalculationResult getPacketLossRatePercentage(
                @NonNull IpSecTransformState oldState,
                @NonNull IpSecTransformState newState,
                int maxSeqNumIncreasePerSecond,
                String logPrefix) {
            logVIpSecTransform("oldState", oldState, logPrefix);
            logVIpSecTransform("newState", newState, logPrefix);

            final int replayWindowSize = oldState.getReplayBitmap().length * 8;
            final long oldSeqHi = oldState.getRxHighestSequenceNumber();
            final long oldSeqLow = Math.max(0L, oldSeqHi - replayWindowSize + 1);
            final long newSeqHi = newState.getRxHighestSequenceNumber();
            final long newSeqLow = Math.max(0L, newSeqHi - replayWindowSize + 1);

            if (oldSeqHi == newSeqHi || newSeqHi < replayWindowSize) {
                // The replay window did not proceed and all packets might have been delivered out
                // of order
                return PacketLossCalculationResult.invalid();
            }

            boolean isUnusualSeqNumLeap = false;

            // Handle sequence number leap
            if (Flags.handleSeqNumLeap()
                    && maxSeqNumIncreasePerSecond != MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED) {
                final long timeDiffMillis =
                        newState.getTimestampMillis() - oldState.getTimestampMillis();
                final long maxSeqNumIncrease = timeDiffMillis * maxSeqNumIncreasePerSecond / 1000;

                // Sequence numbers are unsigned 32-bit values. If maxSeqNumIncrease overflows,
                // isUnusualSeqNumLeap can never be true.
                if (maxSeqNumIncrease >= 0 && newSeqHi - oldSeqHi >= maxSeqNumIncrease) {
                    isUnusualSeqNumLeap = true;
                }
            }

            // Get the expected packet count by assuming there is no packet loss. In this case, SA
            // should receive all packets whose sequence numbers are smaller than the lower bound of
            // the replay window AND the packets received within the window.
            // When the lower bound is 0, it's not possible to tell whether packet with seqNo 0 is
            // received or not. For simplicity just assume that packet is received.
            final long newExpectedPktCnt = newSeqLow + getPacketCntInReplayWindow(newState);
            final long oldExpectedPktCnt = oldSeqLow + getPacketCntInReplayWindow(oldState);

            final long expectedPktCntDiff = newExpectedPktCnt - oldExpectedPktCnt;
            final long actualPktCntDiff = newState.getPacketCount() - oldState.getPacketCount();

            logV(
                    TAG,
                    logPrefix
                            + " expectedPktCntDiff: "
                            + expectedPktCntDiff
                            + " actualPktCntDiff: "
                            + actualPktCntDiff);

            if (Flags.handleSeqNumLeap() && expectedPktCntDiff < MIN_VALID_EXPECTED_RX_PACKET_NUM) {
                // The sample size is too small to ensure a reliable detection result
                return PacketLossCalculationResult.invalid();
            }

            if (expectedPktCntDiff < 0
                    || expectedPktCntDiff == 0
                    || actualPktCntDiff < 0
                    || actualPktCntDiff > expectedPktCntDiff) {
                logWtf(TAG, "Impossible values for expectedPktCntDiff or" + " actualPktCntDiff");
                return PacketLossCalculationResult.invalid();
            }

            final int percent = 100 - (int) (actualPktCntDiff * 100 / expectedPktCntDiff);
            return isUnusualSeqNumLeap
                    ? PacketLossCalculationResult.unusualSeqNumLeap(percent)
                    : PacketLossCalculationResult.valid(percent);
        }
    }

    private static void logVIpSecTransform(
            String transformTag, IpSecTransformState state, String logPrefix) {
        final String stateString =
                " seqNo: "
                        + state.getRxHighestSequenceNumber()
                        + " | pktCnt: "
                        + state.getPacketCount()
                        + " | pktCntInWindow: "
                        + getPacketCntInReplayWindow(state);
        logV(TAG, logPrefix + " " + transformTag + stateString);
    }

    /** Get the number of received packets within the replay window */
    private static long getPacketCntInReplayWindow(@NonNull IpSecTransformState state) {
        return BitSet.valueOf(state.getReplayBitmap()).cardinality();
    }

    @VisibleForTesting(visibility = Visibility.PRIVATE)
    public static class PacketLossCalculationResult {
        @PacketLossResultType private final int mResultType;
        private final int mPacketLossRatePercent;

        private PacketLossCalculationResult(@PacketLossResultType int type, int percent) {
            mResultType = type;
            mPacketLossRatePercent = percent;
        }

        /** Construct an instance that contains a valid packet loss rate */
        public static PacketLossCalculationResult valid(int percent) {
            return new PacketLossCalculationResult(PACKET_LOSS_RATE_VALID, percent);
        }

        /** Construct an instance indicating the inability to get a valid packet loss rate */
        public static PacketLossCalculationResult invalid() {
            return new PacketLossCalculationResult(
                    PACKET_LOSS_RATE_INVALID, PACKET_LOSS_PERCENT_UNAVAILABLE);
        }

        /** Construct an instance indicating that there is an unusual sequence number leap */
        public static PacketLossCalculationResult unusualSeqNumLeap(int percent) {
            return new PacketLossCalculationResult(PACKET_LOSS_UNUSUAL_SEQ_NUM_LEAP, percent);
        }

        @PacketLossResultType
        public int getResultType() {
            return mResultType;
        }

        public int getPacketLossRatePercent() {
            return mPacketLossRatePercent;
        }

        @Override
        public int hashCode() {
            return Objects.hash(mResultType, mPacketLossRatePercent);
        }

        @Override
        public boolean equals(@Nullable Object other) {
            if (!(other instanceof PacketLossCalculationResult)) {
                return false;
            }

            final PacketLossCalculationResult rhs = (PacketLossCalculationResult) other;
            return mResultType == rhs.mResultType
                    && mPacketLossRatePercent == rhs.mPacketLossRatePercent;
        }

        @Override
        public String toString() {
            return "mResultType: "
                    + mResultType
                    + " | mPacketLossRatePercent: "
                    + mPacketLossRatePercent;
        }
    }
}