Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 819f8f7d authored by Chiachang Wang's avatar Chiachang Wang Committed by android-build-merger
Browse files

Add class to support TcpInfo parsing am: ac1bf661 am: 657fe909

am: f29f99e0

Change-Id: I94b001b8a398d5d6a82ef93d0d24f0aae0fd9871
parents e06a3f5e f29f99e0
Loading
Loading
Loading
Loading
+231 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.networkstack.netlink;

import android.util.Log;
import android.util.Range;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.internal.annotations.VisibleForTesting;

import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Objects;

/**
 * Class for tcp_info.
 *
 * Corresponds to {@code struct tcp_info} from bionic/libc/kernel/uapi/linux/tcp.h
 */
public class TcpInfo {
    public enum Field {
        STATE(Byte.BYTES),
        CASTATE(Byte.BYTES),
        RETRANSMITS(Byte.BYTES),
        PROBES(Byte.BYTES),
        BACKOFF(Byte.BYTES),
        OPTIONS(Byte.BYTES),
        WSCALE(Byte.BYTES),
        DELIVERY_RATE_APP_LIMITED(Byte.BYTES),
        RTO(Integer.BYTES),
        ATO(Integer.BYTES),
        SND_MSS(Integer.BYTES),
        RCV_MSS(Integer.BYTES),
        UNACKED(Integer.BYTES),
        SACKED(Integer.BYTES),
        LOST(Integer.BYTES),
        RETRANS(Integer.BYTES),
        FACKETS(Integer.BYTES),
        LAST_DATA_SENT(Integer.BYTES),
        LAST_ACK_SENT(Integer.BYTES),
        LAST_DATA_RECV(Integer.BYTES),
        LAST_ACK_RECV(Integer.BYTES),
        PMTU(Integer.BYTES),
        RCV_SSTHRESH(Integer.BYTES),
        RTT(Integer.BYTES),
        RTTVAR(Integer.BYTES),
        SND_SSTHRESH(Integer.BYTES),
        SND_CWND(Integer.BYTES),
        ADVMSS(Integer.BYTES),
        REORDERING(Integer.BYTES),
        RCV_RTT(Integer.BYTES),
        RCV_SPACE(Integer.BYTES),
        TOTAL_RETRANS(Integer.BYTES),
        PACING_RATE(Long.BYTES),
        MAX_PACING_RATE(Long.BYTES),
        BYTES_ACKED(Long.BYTES),
        BYTES_RECEIVED(Long.BYTES),
        SEGS_OUT(Integer.BYTES),
        SEGS_IN(Integer.BYTES),
        NOTSENT_BYTES(Integer.BYTES),
        MIN_RTT(Integer.BYTES),
        DATA_SEGS_IN(Integer.BYTES),
        DATA_SEGS_OUT(Integer.BYTES),
        DELIVERY_RATE(Long.BYTES),
        BUSY_TIME(Long.BYTES),
        RWND_LIMITED(Long.BYTES),
        SNDBUF_LIMITED(Long.BYTES);

        public final int size;

        Field(int s) {
            size = s;
        }
    }

    private static final String TAG = "TcpInfo";
    private final LinkedHashMap<Field, Number> mFieldsValues = new LinkedHashMap<Field, Number>();

    private TcpInfo(@NonNull ByteBuffer bytes, int infolen) {
        final int start = bytes.position();
        for (final Field field : Field.values()) {
            switch (field.size) {
                case Byte.BYTES:
                    mFieldsValues.put(field, getByte(bytes, start, infolen));
                    break;
                case Integer.BYTES:
                    mFieldsValues.put(field, getInt(bytes, start, infolen));
                    break;
                case Long.BYTES:
                    mFieldsValues.put(field, getLong(bytes, start, infolen));
                    break;
                default:
                    Log.e(TAG, "Unexpected size:" + field.size);
            }
        }

    }

    @VisibleForTesting
    public TcpInfo(@NonNull HashMap<Field, Number> info) {
        for (final Field field : Field.values()) {
            mFieldsValues.put(field, info.get(field));
        }
    }

    /** Parse a TcpInfo from a giving ByteBuffer with a specific length. */
    @Nullable
    public static TcpInfo parse(@NonNull ByteBuffer bytes, int infolen) {
        try {
            TcpInfo info = new TcpInfo(bytes, infolen);
            return info;
        } catch (BufferUnderflowException e) {
            Log.e(TAG, "parsing error.", e);
            return null;
        }
    }

    /**
     * Helper function for handling different struct tcp_info versions in the kernel.
     */
    private static boolean isValidOffset(int start, int len, int pos, int targetBytes) {
        final Range a = new Range(start, start + len);
        final Range b = new Range(pos, pos + targetBytes);
        return a.contains(b);
    }

    /** Get value for specific key. */
    @Nullable
    public Number getValue(@NonNull Field key) {
        return mFieldsValues.get(key);
    }

    @Nullable
    private static Byte getByte(@NonNull ByteBuffer buffer, int start, int len) {
        if (!isValidOffset(start, len, buffer.position(), Byte.BYTES)) return null;

        return buffer.get();
    }

    @Nullable
    private static Integer getInt(@NonNull ByteBuffer buffer, int start, int len) {
        if (!isValidOffset(start, len, buffer.position(), Integer.BYTES)) return null;

        return buffer.getInt();
    }

    @Nullable
    private static Long getLong(@NonNull ByteBuffer buffer, int start, int len) {
        if (!isValidOffset(start, len, buffer.position(), Long.BYTES)) return null;

        return buffer.getLong();
    }

    private static String decodeWscale(byte num) {
        return String.valueOf((num >> 4) & 0x0f)  + ":" + String.valueOf(num & 0x0f);
    }

    /**
     *  Returns a string representing a given tcp state.
     *  Map to enum in bionic/libc/include/netinet/tcp.h
     */
    @VisibleForTesting
    public static String getTcpStateName(int state) {
        switch (state) {
            case 1: return "TCP_ESTABLISHED";
            case 2: return "TCP_SYN_SENT";
            case 3: return "TCP_SYN_RECV";
            case 4: return "TCP_FIN_WAIT1";
            case 5: return "TCP_FIN_WAIT2";
            case 6: return "TCP_TIME_WAIT";
            case 7: return "TCP_CLOSE";
            case 8: return "TCP_CLOSE_WAIT";
            case 9: return "TCP_LAST_ACK";
            case 10: return "TCP_LISTEN";
            case 11: return "TCP_CLOSING";
            default: return "UNKNOWN:" + Integer.toString(state);
        }
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof TcpInfo)) return false;
        TcpInfo other = (TcpInfo) obj;

        for (final Field key : mFieldsValues.keySet()) {
            if (!Objects.equals(mFieldsValues.get(key), other.mFieldsValues.get(key))) {
                return false;
            }
        }
        return true;
    }

    @Override
    public int hashCode() {
        return Objects.hash(mFieldsValues.values().toArray());
    }

    @Override
    public String toString() {
        String str = "TcpInfo{ ";
        for (final Field key : mFieldsValues.keySet()) {
            str += key.name().toLowerCase() + "=";
            if (key == Field.STATE) {
                str += getTcpStateName(mFieldsValues.get(key).intValue()) + " ";
            } else if (key == Field.WSCALE) {
                str += decodeWscale(mFieldsValues.get(key).byteValue()) + " ";
            } else {
                str += mFieldsValues.get(key) + " ";
            }
        }
        str += "}";
        return str;
    }
}
+227 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.networkstack.netlink;

import static org.junit.Assert.assertEquals;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.networkstack.netlink.TcpInfo;

import libcore.util.HexEncoding;

import org.junit.Test;
import org.junit.runner.RunWith;

import java.nio.ByteBuffer;
import java.util.HashMap;

@RunWith(AndroidJUnit4.class)
@SmallTest
public class TcpInfoTest {
    private static final int TCP_INFO_LENGTH_V1 = 192;
    private static final int SHORT_TEST_TCP_INFO = 8;
    private static final String TCP_ESTABLISHED = "TCP_ESTABLISHED";
    private static final String TCP_FIN_WAIT1 = "TCP_FIN_WAIT1";
    private static final String TCP_SYN_SENT = "TCP_SYN_SENT";
    private static final String UNKNOWN_20 = "UNKNOWN:20";
    // Refer to rfc793 for the value definition.
    private static final String TCP_INFO_HEX =
            "01" +                // state = TCP_ESTABLISHED
            "00" +                // ca_state = TCP_CA_OPEN
            "00" +                // retransmits = 0
            "00" +                // probes = 0
            "00" +                // backoff = 0
            "07" +                // option = TCPI_OPT_WSCALE|TCPI_OPT_SACK|TCPI_OPT_TIMESTAMPS
            "88" +                // wscale = 8
            "00" +                // delivery_rate_app_limited = 0
            "001B914A" +          // rto = 1806666
            "00000000" +          // ato = 0
            "0000052E" +          // sndMss = 1326
            "00000218" +          // rcvMss = 536
            "00000000" +          // unsacked = 0
            "00000000" +          // acked = 0
            "00000000" +          // lost = 0
            "00000000" +          // retrans = 0
            "00000000" +          // fackets = 0
            "000000BB" +          // lastDataSent = 187
            "00000000" +          // lastAckSent = 0
            "000000BB" +          // lastDataRecv = 187
            "000000BB" +          // lastDataAckRecv = 187
            "000005DC" +          // pmtu = 1500
            "00015630" +          // rcvSsthresh = 87600
            "00092C3E" +          // rttt = 601150
            "0004961F" +          // rttvar = 300575
            "00000578" +          // sndSsthresh = 1400
            "0000000A" +          // sndCwnd = 10
            "000005A8" +          // advmss = 1448
            "00000003" +          // reordering = 3
            "00000000" +          // rcvrtt = 0
            "00015630" +          // rcvspace = 87600
            "00000000" +          // totalRetrans = 0
            "000000000000AC53" +  // pacingRate = 44115
            "FFFFFFFFFFFFFFFF" +  // maxPacingRate = 18446744073709551615
            "0000000000000001" +  // bytesAcked = 1
            "0000000000000000" +  // bytesReceived = 0
            "00000002" +          // SegsOut = 2
            "00000001" +          // SegsIn = 1
            "00000000" +          // NotSentBytes = 0
            "00092C3E" +          // minRtt = 601150
            "00000000" +          // DataSegsIn = 0
            "00000000" +          // DataSegsOut = 0
            "0000000000000000" +  // deliverRate = 0
            "0000000000000000" +  // busyTime = 0
            "0000000000000000" +  // RwndLimited = 0
            "0000000000000000";   // sndBufLimited = 0
    private static final byte[] TCP_INFO_BYTES =
            HexEncoding.decode(TCP_INFO_HEX.toCharArray(), false);

    @Test
    public void testParseTcpInfo() {
        final ByteBuffer buffer = ByteBuffer.wrap(TCP_INFO_BYTES);
        final HashMap<TcpInfo.Field, Number> expected = makeTestTcpInfoHash();
        final TcpInfo parsedInfo = TcpInfo.parse(buffer, TCP_INFO_LENGTH_V1);

        assertEquals(parsedInfo, new TcpInfo(expected));
    }

    @Test
    public void testValidOffset() {
        final ByteBuffer buffer = ByteBuffer.wrap(TCP_INFO_BYTES);

        final HashMap<TcpInfo.Field, Number> expected = makeShortTestTcpInfoHash();
        final TcpInfo parsedInfo = TcpInfo.parse(buffer, SHORT_TEST_TCP_INFO);

        assertEquals(parsedInfo, new TcpInfo(expected));
    }

    @Test
    public void testTcpStateName() {
        assertEquals(TcpInfo.getTcpStateName(4), TCP_FIN_WAIT1);
        assertEquals(TcpInfo.getTcpStateName(1), TCP_ESTABLISHED);
        assertEquals(TcpInfo.getTcpStateName(2), TCP_SYN_SENT);
        assertEquals(TcpInfo.getTcpStateName(20), UNKNOWN_20);
    }

    private static final String MALFORMED_TCP_INFO_HEX =
            "01" +                // state = TCP_ESTABLISHED
            "00" +                // ca_state = TCP_CA_OPEN
            "00" +                // retransmits = 0
            "00" +                // probes = 0
            "00" +                // backoff = 0
            "07" +                // option = TCPI_OPT_WSCALE|TCPI_OPT_SACK|TCPI_OPT_TIMESTAMPS
            "88" +                // wscale = 8
            "00" +                // delivery_rate_app_limited = 0
            "001B";               // Incomplete bytes, expect to be an int.
    private static final byte[] MALFORMED_TCP_INFO_BYTES =
            HexEncoding.decode(MALFORMED_TCP_INFO_HEX.toCharArray(), false);
    @Test
    public void testMalformedTcpInfo() {
        final ByteBuffer buffer = ByteBuffer.wrap(MALFORMED_TCP_INFO_BYTES);
        final HashMap<TcpInfo.Field, Number> expected = makeShortTestTcpInfoHash();

        TcpInfo parsedInfo = TcpInfo.parse(buffer, SHORT_TEST_TCP_INFO);
        assertEquals(parsedInfo, new TcpInfo(expected));

        parsedInfo = TcpInfo.parse(buffer, TCP_INFO_LENGTH_V1);
        assertEquals(parsedInfo, null);
    }

    @Test
    public void testGetValue() {
        ByteBuffer buffer = ByteBuffer.wrap(TCP_INFO_BYTES);

        final HashMap<TcpInfo.Field, Number> expected = makeShortTestTcpInfoHash();
        expected.put(TcpInfo.Field.MAX_PACING_RATE, 10_000L);
        expected.put(TcpInfo.Field.FACKETS, 10);

        final TcpInfo expectedInfo = new TcpInfo(expected);
        assertEquals((byte) 0x01, expectedInfo.getValue(TcpInfo.Field.STATE));
        assertEquals((byte) 0x00, expectedInfo.getValue(TcpInfo.Field.CASTATE));
        assertEquals((byte) 0x00, expectedInfo.getValue(TcpInfo.Field.RETRANSMITS));
        assertEquals((byte) 0x00, expectedInfo.getValue(TcpInfo.Field.PROBES));
        assertEquals((byte) 0x00, expectedInfo.getValue(TcpInfo.Field.BACKOFF));
        assertEquals((byte) 0x07, expectedInfo.getValue(TcpInfo.Field.OPTIONS));
        assertEquals((byte) 0x88, expectedInfo.getValue(TcpInfo.Field.WSCALE));
        assertEquals((byte) 0x00, expectedInfo.getValue(TcpInfo.Field.DELIVERY_RATE_APP_LIMITED));

        assertEquals(10_000L, expectedInfo.getValue(TcpInfo.Field.MAX_PACING_RATE));
        assertEquals(10, expectedInfo.getValue(TcpInfo.Field.FACKETS));
        assertEquals(null, expectedInfo.getValue(TcpInfo.Field.RTT));

    }

    // Make a TcpInfo contains only first 8 bytes.
    private HashMap<TcpInfo.Field, Number> makeShortTestTcpInfoHash() {
        final HashMap<TcpInfo.Field, Number> info = new HashMap<TcpInfo.Field, Number>();
        info.put(TcpInfo.Field.STATE, (byte) 0x01);
        info.put(TcpInfo.Field.CASTATE, (byte) 0x00);
        info.put(TcpInfo.Field.RETRANSMITS, (byte) 0x00);
        info.put(TcpInfo.Field.PROBES, (byte) 0x00);
        info.put(TcpInfo.Field.BACKOFF, (byte) 0x00);
        info.put(TcpInfo.Field.OPTIONS, (byte) 0x07);
        info.put(TcpInfo.Field.WSCALE, (byte) 0x88);
        info.put(TcpInfo.Field.DELIVERY_RATE_APP_LIMITED, (byte) 0x00);

        return info;
    }

    private HashMap<TcpInfo.Field, Number> makeTestTcpInfoHash() {
        final HashMap<TcpInfo.Field, Number> info = makeShortTestTcpInfoHash();
        info.put(TcpInfo.Field.RTO, 1806666);
        info.put(TcpInfo.Field.ATO, 0);
        info.put(TcpInfo.Field.SND_MSS, 1326);
        info.put(TcpInfo.Field.RCV_MSS, 536);
        info.put(TcpInfo.Field.UNACKED, 0);
        info.put(TcpInfo.Field.SACKED, 0);
        info.put(TcpInfo.Field.LOST, 0);
        info.put(TcpInfo.Field.RETRANS, 0);
        info.put(TcpInfo.Field.FACKETS, 0);
        info.put(TcpInfo.Field.LAST_DATA_SENT, 187);
        info.put(TcpInfo.Field.LAST_ACK_SENT, 0);
        info.put(TcpInfo.Field.LAST_DATA_RECV, 187);
        info.put(TcpInfo.Field.LAST_ACK_RECV, 187);
        info.put(TcpInfo.Field.PMTU, 1500);
        info.put(TcpInfo.Field.RCV_SSTHRESH, 87600);
        info.put(TcpInfo.Field.RTT, 601150);
        info.put(TcpInfo.Field.RTTVAR, 300575);
        info.put(TcpInfo.Field.SND_SSTHRESH, 1400);
        info.put(TcpInfo.Field.SND_CWND, 10);
        info.put(TcpInfo.Field.ADVMSS, 1448);
        info.put(TcpInfo.Field.REORDERING, 3);
        info.put(TcpInfo.Field.RCV_RTT, 0);
        info.put(TcpInfo.Field.RCV_SPACE, 87600);
        info.put(TcpInfo.Field.TOTAL_RETRANS, 0);
        info.put(TcpInfo.Field.PACING_RATE, 44115L);
        info.put(TcpInfo.Field.MAX_PACING_RATE, -1L);
        info.put(TcpInfo.Field.BYTES_ACKED, 1L);
        info.put(TcpInfo.Field.BYTES_RECEIVED, 0L);
        info.put(TcpInfo.Field.SEGS_OUT, 2);
        info.put(TcpInfo.Field.SEGS_IN, 1);
        info.put(TcpInfo.Field.NOTSENT_BYTES, 0);
        info.put(TcpInfo.Field.MIN_RTT, 601150);
        info.put(TcpInfo.Field.DATA_SEGS_IN, 0);
        info.put(TcpInfo.Field.DATA_SEGS_OUT, 0);
        info.put(TcpInfo.Field.DELIVERY_RATE, 0L);
        info.put(TcpInfo.Field.BUSY_TIME, 0L);
        info.put(TcpInfo.Field.RWND_LIMITED, 0L);
        info.put(TcpInfo.Field.SNDBUF_LIMITED, 0L);

        return info;
    }
}